#![cfg_attr(docsrs, feature(doc_cfg))]
#![deny(missing_docs)]
#![doc = include_str!("../README.md")]
#![cfg_attr(not(feature = "std"), no_std)]

extern crate alloc;

/// Error types
pub mod error;
pub mod expression;
/// Auto-generated lists of license identifiers and exception identifiers
#[allow(missing_docs)]
pub mod identifiers;
/// Contains types for lexing an SPDX license expression
pub mod lexer;
mod licensee;

/// Allows analysis of text to determine if it resembles a license
#[cfg(feature = "detection")]
pub mod detection;

/// Auto-generated full canonical text of each license
#[cfg(feature = "text")]
#[allow(missing_docs)]
pub mod text;

use alloc::{boxed::Box, string::String};
use core::{
    cmp::{self, Ordering},
    fmt,
};
pub use error::ParseError;
pub use expression::Expression;
pub use lexer::ParseMode;
pub use licensee::Licensee;

/// Flags that can apply to licenses and/or license exceptions
pub mod flags {
    /// Inner type of the flags
    pub type Type = u8;

    /// Whether the license is listed as free by the [Free Software Foundation](https://www.gnu.org/licenses/license-list.en.html)
    pub const IS_FSF_LIBRE: Type = 0x1;
    /// Whether the license complies with the Open Source Definition as determined by the [Open Source Initiative](https://opensource.org/licenses)
    pub const IS_OSI_APPROVED: Type = 0x2;
    /// Whether the license or exception has been deprecated and should no longer be used
    pub const IS_DEPRECATED: Type = 0x4;
    /// Whether the license is considered copyleft
    pub const IS_COPYLEFT: Type = 0x8;
    /// Whether the license is a GNU license
    pub const IS_GNU: Type = 0x10;
}

/// An SPDX license
pub struct License {
    /// The short identifier for the license
    pub name: &'static str,
    /// The full name of the license
    pub full_name: &'static str,
    /// The index in the full license list where this license is positioned
    pub index: usize,
    /// The flags for this license
    pub flags: flags::Type,
}

/// Unique identifier for a particular license
///
/// ```
/// let bsd = spdx::license_id("BSD-3-Clause").unwrap();
///
/// assert!(
///     bsd.is_fsf_free_libre()
///     && bsd.is_osi_approved()
///     && !bsd.is_deprecated()
///     && !bsd.is_copyleft()
/// );
/// ```
#[derive(Copy, Clone)]
pub struct LicenseId {
    l: &'static License,
}

impl PartialEq for LicenseId {
    #[inline]
    fn eq(&self, o: &Self) -> bool {
        self.l.index == o.l.index
    }
}

impl Eq for LicenseId {}

impl Ord for LicenseId {
    #[inline]
    fn cmp(&self, o: &Self) -> Ordering {
        self.l.index.cmp(&o.l.index)
    }
}

impl PartialOrd for LicenseId {
    #[inline]
    fn partial_cmp(&self, o: &Self) -> Option<Ordering> {
        Some(self.cmp(o))
    }
}

impl core::ops::Deref for LicenseId {
    type Target = License;

    #[inline]
    fn deref(&self) -> &Self::Target {
        self.l
    }
}

impl LicenseId {
    /// Returns true if the license is [considered free by the FSF](https://www.gnu.org/licenses/license-list.en.html)
    ///
    /// ```
    /// assert!(spdx::license_id("GPL-2.0-only").unwrap().is_fsf_free_libre());
    /// ```
    #[inline]
    #[must_use]
    pub fn is_fsf_free_libre(self) -> bool {
        self.l.flags & flags::IS_FSF_LIBRE != 0
    }

    /// Returns true if the license is [OSI approved](https://opensource.org/licenses)
    ///
    /// ```
    /// assert!(spdx::license_id("MIT").unwrap().is_osi_approved());
    /// ```
    #[inline]
    #[must_use]
    pub fn is_osi_approved(self) -> bool {
        self.l.flags & flags::IS_OSI_APPROVED != 0
    }

    /// Returns true if the license is deprecated
    ///
    /// ```
    /// assert!(spdx::license_id("wxWindows").unwrap().is_deprecated());
    /// ```
    #[inline]
    #[must_use]
    pub fn is_deprecated(self) -> bool {
        self.l.flags & flags::IS_DEPRECATED != 0
    }

    /// Returns true if the license is [copyleft](https://en.wikipedia.org/wiki/Copyleft)
    ///
    /// ```
    /// assert!(spdx::license_id("LGPL-3.0-or-later").unwrap().is_copyleft());
    /// ```
    #[inline]
    #[must_use]
    pub fn is_copyleft(self) -> bool {
        self.l.flags & flags::IS_COPYLEFT != 0
    }

    /// Returns true if the license is a [GNU license](https://www.gnu.org/licenses/identify-licenses-clearly.html),
    /// which operate differently than all other SPDX license identifiers
    ///
    /// ```
    /// assert!(spdx::license_id("AGPL-3.0-only").unwrap().is_gnu());
    /// ```
    #[inline]
    #[must_use]
    pub fn is_gnu(self) -> bool {
        self.l.flags & flags::IS_GNU != 0
    }

    /// Retrieves the version of the license ID, if any
    ///
    /// ```
    /// assert_eq!(spdx::license_id("GPL-2.0-only").unwrap().version().unwrap(), "2.0");
    /// assert_eq!(spdx::license_id("BSD-3-Clause").unwrap().version().unwrap(), "3");
    /// assert!(spdx::license_id("Aladdin").unwrap().version().is_none());
    /// ```
    #[inline]
    pub fn version(self) -> Option<&'static str> {
        self.l
            .name
            .split('-')
            .find(|comp| comp.chars().all(|c| c == '.' || c.is_ascii_digit()))
    }

    /// The base name of the license
    ///
    /// ```
    /// assert_eq!(spdx::license_id("GPL-2.0-only").unwrap().base(), "GPL");
    /// assert_eq!(spdx::license_id("MIT").unwrap().base(), "MIT");
    /// ```
    #[inline]
    pub fn base(self) -> &'static str {
        self.l.name.split_once('-').map_or(self.l.name, |(n, _)| n)
    }

    /// Attempts to retrieve the license text
    ///
    /// ```
    /// assert!(spdx::license_id("GFDL-1.3-invariants").unwrap().text().contains("Invariant Sections"))
    /// ```
    #[cfg(feature = "text")]
    #[inline]
    pub fn text(self) -> &'static str {
        text::LICENSE_TEXTS[self.l.index].1
    }
}

impl fmt::Debug for LicenseId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.l.name)
    }
}

/// An SPDX exception
pub struct Exception {
    /// The name of the exception
    pub name: &'static str,
    /// The index in the full exception list where this exception is positioned
    pub index: usize,
    /// The flags for the exception
    pub flags: flags::Type,
}

/// Unique identifier for a particular exception
///
/// ```
/// let exception_id = spdx::exception_id("LLVM-exception").unwrap();
/// assert!(!exception_id.is_deprecated());
/// ```
#[derive(Copy, Clone)]
pub struct ExceptionId {
    e: &'static Exception,
}

impl PartialEq for ExceptionId {
    #[inline]
    fn eq(&self, o: &Self) -> bool {
        self.e.index == o.e.index
    }
}

impl Eq for ExceptionId {}

impl Ord for ExceptionId {
    #[inline]
    fn cmp(&self, o: &Self) -> Ordering {
        self.e.index.cmp(&o.e.index)
    }
}

impl PartialOrd for ExceptionId {
    #[inline]
    fn partial_cmp(&self, o: &Self) -> Option<Ordering> {
        Some(self.cmp(o))
    }
}

impl core::ops::Deref for ExceptionId {
    type Target = Exception;

    #[inline]
    fn deref(&self) -> &Self::Target {
        self.e
    }
}

impl ExceptionId {
    /// Returns true if the exception is deprecated
    ///
    /// ```
    /// assert!(spdx::exception_id("Nokia-Qt-exception-1.1").unwrap().is_deprecated());
    /// ```
    #[inline]
    #[must_use]
    pub fn is_deprecated(self) -> bool {
        self.e.flags & flags::IS_DEPRECATED != 0
    }

    /// Attempts to retrieve the license exception text
    ///
    /// ```
    /// assert!(spdx::exception_id("LLVM-exception").unwrap().text().contains("LLVM Exceptions to the Apache 2.0 License"));
    /// ```
    #[cfg(feature = "text")]
    #[inline]
    pub fn text(self) -> &'static str {
        text::EXCEPTION_TEXTS[self.e.index].1
    }
}

impl fmt::Debug for ExceptionId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.e.name)
    }
}

/// Represents a single license requirement.
///
/// The requirement must include a valid [`LicenseItem`], and may allow current
/// and future versions of the license, and may also allow for a specific exception
///
/// While they can be constructed manually, most of the time these will
/// be parsed and combined in an [Expression]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct LicenseReq {
    /// The license
    pub license: LicenseItem,
    /// The additional text for this license, as specified following
    /// the `WITH` operator
    pub addition: Option<AdditionItem>,
}

impl From<LicenseId> for LicenseReq {
    fn from(id: LicenseId) -> Self {
        Self {
            license: LicenseItem::Spdx {
                id,
                or_later: false,
            },
            addition: None,
        }
    }
}

impl fmt::Display for LicenseReq {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
        self.license.fmt(f)?;

        if let Some(ref exe) = self.addition {
            write!(f, " WITH {exe}")?;
        }

        Ok(())
    }
}

/// SPDX allows the use of `LicenseRef-<user supplied string>` to provide
/// arbitrary licenses that aren't a part of the official SPDX license list
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct LicenseRef {
    /// Identify any external SPDX documents referenced within this SPDX document.
    ///
    /// See the [spec](https://spdx.org/spdx-specification-21-web-version#h.h430e9ypa0j9) for
    /// more details.
    pub doc_ref: Option<String>,
    /// Provide a locally unique identifier to refer to licenses that are not found on the SPDX License List.
    ///
    /// See the [spec](https://spdx.org/spdx-specification-21-web-version#h.4f1mdlm) for
    /// more details.
    pub lic_ref: String,
}

impl fmt::Display for LicenseRef {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
        match (&self.doc_ref, &self.lic_ref) {
            (Some(d), a) => write!(f, "DocumentRef-{d}:LicenseRef-{a}"),
            (None, a) => write!(f, "LicenseRef-{a}"),
        }
    }
}

/// A single license term in a license expression, according to the SPDX spec.
///
/// This can be either an SPDX license, which is mapped to a [`LicenseId`] from
/// a valid SPDX short identifier, or else a document and/or license ref
#[derive(Debug, Clone)]
pub enum LicenseItem {
    /// A regular SPDX license id
    Spdx {
        /// The license identifier
        id: LicenseId,
        /// Indicates the license had a `+`, allowing the licensee to license
        /// the software under either the specific version, or any later versions
        or_later: bool,
    },
    /// SPDX allows the use of `LicenseRef-<user supplied string>` to provide
    /// arbitrary licenses that aren't a part of the official SPDX license list
    Other(Box<LicenseRef>),
}

impl LicenseItem {
    /// Returns the license identifier, if it is a recognized SPDX license and not
    /// a license referencer
    #[must_use]
    pub fn id(&self) -> Option<LicenseId> {
        match self {
            Self::Spdx { id, .. } => Some(*id),
            Self::Other { .. } => None,
        }
    }
}

impl Ord for LicenseItem {
    fn cmp(&self, o: &Self) -> Ordering {
        match (self, o) {
            (
                Self::Spdx {
                    id: a,
                    or_later: la,
                },
                Self::Spdx {
                    id: b,
                    or_later: lb,
                },
            ) => match a.cmp(b) {
                Ordering::Equal => la.cmp(lb),
                o => o,
            },
            (Self::Other(a), Self::Other(b)) => a.cmp(b),
            (Self::Spdx { .. }, Self::Other { .. }) => Ordering::Less,
            (Self::Other { .. }, Self::Spdx { .. }) => Ordering::Greater,
        }
    }
}

#[allow(clippy::non_canonical_partial_ord_impl)]
impl PartialOrd for LicenseItem {
    fn partial_cmp(&self, o: &Self) -> Option<Ordering> {
        match (self, o) {
            (Self::Spdx { id: a, .. }, Self::Spdx { id: b, .. }) => a.partial_cmp(b),
            (Self::Other(a), Self::Other(b)) => a.partial_cmp(b),
            (Self::Spdx { .. }, Self::Other { .. }) => Some(cmp::Ordering::Less),
            (Self::Other { .. }, Self::Spdx { .. }) => Some(cmp::Ordering::Greater),
        }
    }
}

impl PartialEq for LicenseItem {
    fn eq(&self, o: &Self) -> bool {
        matches!(self.partial_cmp(o), Some(cmp::Ordering::Equal))
    }
}

impl Eq for LicenseItem {}

impl fmt::Display for LicenseItem {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
        match self {
            LicenseItem::Spdx { id, or_later } => {
                id.name.fmt(f)?;

                if *or_later {
                    if id.is_gnu() && id.is_deprecated() {
                        f.write_str("-or-later")?;
                    } else if !id.is_gnu() {
                        f.write_str("+")?;
                    }
                }

                Ok(())
            }
            LicenseItem::Other(refs) => refs.fmt(f),
        }
    }
}

/// A user supplied `AddtionRef-<user string>` to specify additional text to
/// associate with a license that falls outside the SPDX license list
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct AdditionRef {
    /// Purpose: Identify any external SPDX documents referenced within this SPDX document.
    /// See the [spec](https://spdx.org/spdx-specification-21-web-version#h.h430e9ypa0j9) for
    /// more details.
    pub doc_ref: Option<String>,
    /// Purpose: Provide a locally unique identifier to refer to additional text that are not found on the SPDX License List.
    /// See the [spec](https://spdx.org/spdx-specification-21-web-version#h.4f1mdlm) for
    /// more details.
    pub add_ref: String,
}

impl fmt::Display for AdditionRef {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
        match (&self.doc_ref, &self.add_ref) {
            (Some(d), a) => write!(f, "DocumentRef-{d}:AdditionRef-{a}"),
            (None, a) => write!(f, "AdditionRef-{a}"),
        }
    }
}

/// A single addition term in a addition expression, according to the SPDX spec.
///
/// This can be either an SPDX license exception, which is mapped to a [`ExceptionId`]
/// from a valid SPDX short identifier, or else a document and/or addition ref
#[derive(Debug, Clone)]
pub enum AdditionItem {
    /// A regular SPDX license exception id
    Spdx(ExceptionId),
    /// A user supplied `AddtionRef-<user string>` to specify additional text to
    /// associate with a license that falls outside the SPDX license list
    Other(Box<AdditionRef>),
}

impl AdditionItem {
    /// Returns the license exception identifier, if it is a recognized SPDX license exception
    /// and not a license exception referencer
    #[must_use]
    pub fn id(&self) -> Option<ExceptionId> {
        match self {
            Self::Spdx(id) => Some(*id),
            Self::Other { .. } => None,
        }
    }
}

impl Ord for AdditionItem {
    fn cmp(&self, o: &Self) -> Ordering {
        match (self, o) {
            (Self::Spdx(a), Self::Spdx(b)) => match a.cmp(b) {
                Ordering::Equal => a.cmp(b),
                o => o,
            },
            (Self::Other(a), Self::Other(b)) => a.cmp(b),
            (Self::Spdx(_), Self::Other { .. }) => Ordering::Less,
            (Self::Other { .. }, Self::Spdx(_)) => Ordering::Greater,
        }
    }
}

#[allow(clippy::non_canonical_partial_ord_impl)]
impl PartialOrd for AdditionItem {
    fn partial_cmp(&self, o: &Self) -> Option<Ordering> {
        match (self, o) {
            (Self::Spdx(a), Self::Spdx(b)) => a.partial_cmp(b),
            (Self::Other(a), Self::Other(b)) => a.partial_cmp(b),
            (Self::Spdx(_), Self::Other { .. }) => Some(cmp::Ordering::Less),
            (Self::Other { .. }, Self::Spdx(_)) => Some(cmp::Ordering::Greater),
        }
    }
}

impl PartialEq for AdditionItem {
    fn eq(&self, o: &Self) -> bool {
        matches!(self.partial_cmp(o), Some(cmp::Ordering::Equal))
    }
}
impl Eq for AdditionItem {}

impl fmt::Display for AdditionItem {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
        match self {
            AdditionItem::Spdx(id) => id.name.fmt(f),
            AdditionItem::Other(refs) => refs.fmt(f),
        }
    }
}

/// Attempts to find a [`LicenseId`] given a short id.
///
/// Note that any `+` at the end is trimmed when searching for a match.
///
/// ```
/// assert!(spdx::license_id("MIT").is_some());
/// assert!(spdx::license_id("BitTorrent-1.1+").is_some());
/// ```
#[inline]
#[must_use]
pub fn license_id(name: &str) -> Option<LicenseId> {
    let name = name.trim_end_matches('+');
    identifiers::LICENSES
        .binary_search_by(|lic| lic.name.cmp(name))
        .map(|index| LicenseId {
            l: &identifiers::LICENSES[index],
        })
        .ok()
}

/// Attempts to find a GNU license from its base name.
///
/// GNU licenses are "special", unlike every other license in the SPDX list, they
/// have (in _most_ cases) a bare variant which is deprecated, eg. GPL-2.0, an
/// `-only` variant which acts like every other license, and an `-or-later`
/// variant which acts as if `+` was applied.
#[inline]
#[must_use]
pub fn gnu_license_id(base: &str, or_later: bool) -> Option<LicenseId> {
    if base.ends_with("-only") || base.ends_with("-or-later") {
        license_id(base)
    } else {
        let mut v = smallvec::SmallVec::<[u8; 32]>::new();
        v.resize(base.len() + if or_later { 9 } else { 5 }, 0);

        v[..base.len()].copy_from_slice(base.as_bytes());

        if or_later {
            v[base.len()..].copy_from_slice(b"-or-later");
        } else {
            v[base.len()..].copy_from_slice(b"-only");
        }

        let Ok(s) = core::str::from_utf8(v.as_slice()) else {
            // Unreachable, but whatever
            return None;
        };
        license_id(s)
    }
}

/// Find license partially matching the name, e.g. "apache" => "Apache-2.0"
///
/// Returns length (in bytes) of the string matched. Garbage at the end is
/// ignored. See [`crate::identifiers::IMPRECISE_NAMES`] for the list of invalid
/// names, and the valid license identifiers they are mapped to.
///
/// ```
/// assert_eq!(
///     spdx::imprecise_license_id("simplified bsd license").unwrap().0,
///     spdx::license_id("BSD-2-Clause").unwrap()
/// );
/// ```
#[inline]
#[must_use]
pub fn imprecise_license_id(name: &str) -> Option<(LicenseId, usize)> {
    for (prefix, correct_name) in identifiers::IMPRECISE_NAMES {
        if let Some(name_prefix) = name.as_bytes().get(0..prefix.len()) {
            if prefix.as_bytes().eq_ignore_ascii_case(name_prefix) {
                return license_id(correct_name).map(|lic| (lic, prefix.len()));
            }
        }
    }
    None
}

/// Attempts to find an [`ExceptionId`] for the string
///
/// ```
/// assert!(spdx::exception_id("LLVM-exception").is_some());
/// ```
#[inline]
#[must_use]
pub fn exception_id(name: &str) -> Option<ExceptionId> {
    identifiers::EXCEPTIONS
        .binary_search_by(|exc| exc.name.cmp(name))
        .map(|index| ExceptionId {
            e: &identifiers::EXCEPTIONS[index],
        })
        .ok()
}

/// Returns the version number of the SPDX list from which
/// the license and exception identifiers are sourced from
#[inline]
#[must_use]
pub fn license_version() -> &'static str {
    identifiers::VERSION
}

#[cfg(test)]
mod test {
    use super::LicenseItem;
    use crate::{Expression, license_id};
    use alloc::string::ToString;

    #[test]
    fn gnu_or_later_display() {
        let gpl_or_later = LicenseItem::Spdx {
            id: license_id("GPL-3.0").unwrap(),
            or_later: true,
        };

        let gpl_or_later_in_id = LicenseItem::Spdx {
            id: license_id("GPL-3.0-or-later").unwrap(),
            or_later: true,
        };

        let gpl_or_later_parsed = Expression::parse("GPL-3.0-or-later").unwrap();

        let non_gnu_or_later = LicenseItem::Spdx {
            id: license_id("Apache-2.0").unwrap(),
            or_later: true,
        };

        assert_eq!(gpl_or_later.to_string(), "GPL-3.0-or-later");
        assert_eq!(gpl_or_later_parsed.to_string(), "GPL-3.0-or-later");
        assert_eq!(gpl_or_later_in_id.to_string(), "GPL-3.0-or-later");
        assert_eq!(non_gnu_or_later.to_string(), "Apache-2.0+");
    }
}
