// TAMER formatting helpers
//
//  Copyright (C) 2014-2023 Ryan Specialty, LLC.
//
//  This file is part of TAME.
//
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.

//! Typed formatting helpers.
//!
//! These types create composable formatters for use with [`Display`].
//! Whereas [`Display`] operates on data owned by the type implementing it,
//!   these formatters compose into functions that operate on data provided
//!   _to_ it.
//! Consequently,
//!   formatters are simply types,
//!   and writes can be streamed just as they are with [`Display`].
//!
//! There are two types of wrappers:
//!
//!   - [`DisplayWrapper`] formats objects as atoms; and
//!   - [`ListDisplayWrapper`] maps a [`DisplayWrapper`] to each of its
//!       items as atoms,
//!         where the specific wrapper used depends on the position of the
//!         item within the list and the properties of the list itself.
//!
//! [`DisplayWrapper::wrap`] and [`ListDisplayWrapper::wrap`] can be used to
//!   associate wrappers with data,
//!     effectively creating a custom [`Display`] implementation controlled
//!     by the caller.
//! This is _not_ a substitute for canonical object representations that own
//!   their own [`Display`] implementation,
//!     but is suitable where display is context-dependent.
//!
//! For example:
//!
//! ```
//! # use tamer::{fmt::*, xir::fmt::*};
//! let attrs = ["foo", "bar", "baz"];
//!
//! assert_eq!(
//!    AndQualConjList::<"attribute", "attributes", Tt<XmlAttr>>::wrap(&attrs)
//!        .to_string(),
//!    "attributes `@foo`, `@bar`, and `@baz`",
//! );
//!
//! assert_eq!(
//!    AndQualConjList::<"attribute", "attributes", Tt<XmlAttr>>::wrap(&attrs[0..1])
//!        .to_string(),
//!    "attribute `@foo`",
//! );
//!
//! assert_eq!(
//!     AndConjList::<Raw>::wrap(&["toil", "trouble"]).to_string(),
//!     "toil and trouble",
//! );
//! ```
//!
//! This is in contrast to the approach taken by (for example)
//!   the [diagnostic system](crate::diagnose),
//!     which is to produce a data structure representing the data to be
//!     formatted.
//! This system is not useful as,
//!   and is not intended to be,
//!   an IR for more sophisticated manipulation of data prior to output.

use std::{
    fmt::{Display, Formatter, Result},
    marker::PhantomData,
};

/// Wrapper for a [`Display`]-able type.
///
/// See the [module-level documentation](super) for more information.
pub trait DisplayWrapper {
    /// Transform inner data and output using the provided [`Formatter`].
    ///
    /// If a [`Formatter`] is not available,
    ///   [`ListDisplayWrapper::wrap`] may be used to produce a
    ///   [`Display`]-able object instead.
    fn fmt<T: Display>(inner: T, f: &mut Formatter) -> Result;

    /// Associate data with a [`DisplayWrapper`] for rendering using
    ///   [`Display`].
    ///
    /// This has the effect of creating an arbitrary [`Display`]
    ///   implementation for the wrapped object,
    ///     which will work well with [`format!`] and anything else that
    ///     does not have access to an explicit [`Formatter`].
    fn wrap<T: Display>(inner: T) -> Wrap<Self, T> {
        Wrap {
            inner,
            _phantom: Default::default(),
        }
    }
}

/// Wrapper with associated data.
///
/// This has the effect of creating an arbitrary [`Display`] implementation
///   for the wrapped data,
///     which will work well with [`format!`] and anything else that does
///     not have access to an explicit [`Formatter`].
pub struct Wrap<W: DisplayWrapper + ?Sized, T: Display> {
    inner: T,
    _phantom: PhantomData<W>,
}

impl<W: DisplayWrapper, T: Display> Display for Wrap<W, T> {
    fn fmt(&self, f: &mut Formatter) -> Result {
        W::fmt(&self.inner, f)
    }
}

/// Echo data as-is without any wrapping.
///
/// This is primarily used at the root of a wrapper composition.
pub struct Raw;

impl DisplayWrapper for Raw {
    fn fmt<T: Display>(inner: T, f: &mut Formatter) -> Result {
        inner.fmt(f)
    }
}

/// Prefix data with a static [`str`].
///
/// See also [`Suffix`] and [`Delim`].
pub struct Prefix<const PREFIX: &'static str, W: DisplayWrapper>(
    PhantomData<W>,
);

impl<const PREFIX: &'static str, W: DisplayWrapper> DisplayWrapper
    for Prefix<PREFIX, W>
{
    fn fmt<T: Display>(inner: T, f: &mut Formatter) -> Result {
        f.write_str(PREFIX)?;
        W::fmt(inner, f)
    }
}

/// Suffix data with a static [`str`].
///
/// See also [`Prefix`] and [`Delim`].
pub struct Suffix<const SUFFIX: &'static str, W: DisplayWrapper>(
    PhantomData<W>,
);

impl<const SUFFIX: &'static str, W: DisplayWrapper> DisplayWrapper
    for Suffix<SUFFIX, W>
{
    fn fmt<T: Display>(inner: T, f: &mut Formatter) -> Result {
        W::fmt(inner, f)?;
        f.write_str(SUFFIX)
    }
}

/// Surround a value in delimiters.
///
/// See also [`Prefix`] and [`Suffix`].
pub type Delim<const LEFT: &'static str, const RIGHT: &'static str, W> =
    Prefix<LEFT, Suffix<RIGHT, W>>;

/// Denote text that would conventionally be delimited in a teletypewriter
///   font.
///
/// This produces a markdown-style quote using backticks.
///
/// NB: This does not defend against nested quotes,
///   so this is _not_ safe against format escapes.
pub type Tt<W> = Delim<"`", "`", W>;

/// Quote text that would conventionally be delimited in a teletypewriter
///   font.
///
/// This is a more terse alternative to [`Tt`] when formatter composition is
///   unneeded.
pub type TtQuote = Tt<Raw>;

/// Prefix with a single space.
pub type Sp<W> = Prefix<" ", W>;

/// Wrapper for a list that maps each element to a context-specific
///   [`DisplayWrapper`].
///
/// This uses the slice API for wrapping since [`Display`] takes objects by
///   non-mutable reference,
///     and so we cannot consume an iterator.
///
/// The associated types define the wrappers to use for items in various
///   positions depending on the length of the list.
/// They were chosen to be somewhat intuitive given the necessary use cases.
///
/// See the [module-level documentation](super) for examples.
pub trait ListDisplayWrapper {
    /// Wrapper to use when the list contains only a single item.
    type Single: DisplayWrapper = Raw;
    /// Wrapper for the first item in a multi-item list.
    type First: DisplayWrapper = Raw;
    /// Wrapper for all but the first and last items in a multi-item list.
    type Middle: DisplayWrapper = Raw;
    /// Wrapper for the last item of a list containing a pair of items.
    type LastOfPair: DisplayWrapper = Raw;
    /// Wrapper for the last item of a list containing more than two items.
    type LastOfMany: DisplayWrapper = Raw;

    /// Format a slice using the provided wrappers.
    ///
    /// If a [`Formatter`] is not available,
    ///   [`ListDisplayWrapper::wrap`] may be used to produce a
    ///   [`Display`]-able object instead.
    fn fmt<T: Display>(list: &[T], f: &mut Formatter) -> Result {
        let lasti = list.len().saturating_sub(1);

        // This can be further abstracted away using the above primitives,
        //   if ever we have a use.
        for next in list.iter().enumerate() {
            match next {
                (i, x) => Self::fmt_nth(lasti, i, x, f)?,
            };
        }

        Ok(())
    }

    /// Format an item as if it were the `i`th value of a list of length
    ///   `lasti+1`.
    ///
    /// This allows for generating list-like output without the expense of
    ///   actually producing a list.
    /// This may be useful when values are stored in different memory
    ///   location,
    ///     so that the displaying of those values is a problem of invoking
    ///     this method on them in the right order,
    ///       rather than collecting them just for the sake of display.
    /// If Rust supports `const` array/Vec functions in the future,
    ///   this may not be necessary anymore,
    ///     unless we also don't want the space cost of such a
    ///     precomputation
    ///       (but it may come with performance benefits from locality).
    #[inline]
    fn fmt_nth<T: Display>(
        lasti: usize,
        i: usize,
        item: &T,
        f: &mut Formatter,
    ) -> Result {
        match (i, item) {
            (0, x) if lasti == 0 => {
                Self::Single::fmt(x, f)?;
            }

            (0, x) => {
                Self::First::fmt(x, f)?;
            }

            (i, x) if lasti == i => {
                if i == 1 {
                    Self::LastOfPair::fmt(x, f)?;
                } else {
                    Self::LastOfMany::fmt(x, f)?;
                }
            }

            (_, x) => {
                Self::Middle::fmt(x, f)?;
            }
        }

        Ok(())
    }

    /// Associate data with a [`ListDisplayWrapper`] for rendering using
    ///   [`Display`].
    ///
    /// This has the effect of creating an arbitrary [`Display`]
    ///   implementation for the wrapped slice,
    ///     which will work well with [`format!`] and anything else that
    ///     does not have access to an explicit [`Formatter`].
    fn wrap<T: Display>(list: &[T]) -> ListWrap<Self, T> {
        ListWrap {
            list,
            _phantom: Default::default(),
        }
    }
}

/// Format each item of a slice using a [`DisplayWrapper`] formatter,
///   outputting an English list with a serial comma and conjunctive term.
///
/// No formatting is done to a single item,
///   and the serial comma is omitted for only two items.
/// It is assumed that the items are not complete sentences,
///   and nested lists are not expected
///     (in such a case you'd want to replace the inner list with
///       semicolons and a more robust abstraction is needed).
///
/// The use of the serial comma (also known as the Oxford comma) is the
///   preference of the author.
///
/// For example:
///   If we have a slice `[1, 2, 3]`,
///     this will output "1, 2, and 3".
///   If we have a slice `[1, 2]`,
///     it will omit the serial comma and output "1 and 2",
///       since no person would write "1, and 2" unless they wished to
///       place particularly dramatic emphasis on the first item.
///
/// To output a qualifier before the list,
///   see [`QualConjList`].
pub struct ConjList<const CONJ: &'static str, W: DisplayWrapper>(
    PhantomData<W>,
);

impl<const CONJ: &'static str, W: DisplayWrapper> ListDisplayWrapper
    for ConjList<CONJ, W>
{
    type Single = W;
    type First = W;
    type Middle = Prefix<", ", W>;
    type LastOfPair = Sp<Prefix<CONJ, Sp<W>>>;
    // Comma after the penultimate term (serial/Oxford comma).
    type LastOfMany = Prefix<", ", Prefix<CONJ, Sp<W>>>;
}

/// A list of values with a serial comma and the term "and" as a
///   conjunction between the penultimate and final items.
pub type AndConjList<W> = ConjList<"and", W>;
/// A list of values with a serial comma and the term "or" as a
///   conjunction between the penultimate and final items.
///
/// Terminology note:
///   English refers to the term "or" here as a conjunction between words,
///     which differs from "or" in logic as a disjunction.
pub type OrConjList<W> = ConjList<"or", W>;

/// Format each item of a slice using a [`DisplayWrapper`] formatter,
///   outputting an English list with a serial comma and conjunctive term,
///   qualified by a term that is singular or plural depending on the length
///   of the list.
///
/// See [`ConjList`] for more information;
///   this operates identically but with the addition of the qualifier.
pub struct QualConjList<
    const QUAL_ONE: &'static str,
    const QUAL_MANY: &'static str,
    const CONJ: &'static str,
    W: DisplayWrapper,
>(PhantomData<W>);

impl<
        const QUAL_ONE: &'static str,
        const QUAL_MANY: &'static str,
        const CONJ: &'static str,
        W: DisplayWrapper,
    > ListDisplayWrapper for QualConjList<QUAL_ONE, QUAL_MANY, CONJ, W>
{
    type Single =
        Prefix<QUAL_ONE, Sp<<ConjList<CONJ, W> as ListDisplayWrapper>::Single>>;
    type First =
        Prefix<QUAL_MANY, Sp<<ConjList<CONJ, W> as ListDisplayWrapper>::First>>;
    type Middle = <ConjList<CONJ, W> as ListDisplayWrapper>::Middle;
    type LastOfPair = <ConjList<CONJ, W> as ListDisplayWrapper>::LastOfPair;
    type LastOfMany = <ConjList<CONJ, W> as ListDisplayWrapper>::LastOfMany;
}

/// A list of values with a serial comma and the term "and" as a
///   conjunction between the penultimate and final items.
///     prefixed with a singular or plural qualifier term depending on the
///     length of the list.
pub type AndQualConjList<
    const QUAL_ONE: &'static str,
    const QUAL_MANY: &'static str,
    W,
> = QualConjList<QUAL_ONE, QUAL_MANY, "and", W>;

/// A list of values with a serial comma and the term "or" as a
///   conjunction between the penultimate and final items.
///     prefixed with a singular or plural qualifier term depending on the
///     length of the list.
///
/// Terminology note:
///   English refers to the term "or" here as a conjunction between words,
///     which differs from "or" in logic as a disjunction.
pub type OrQualConjList<
    const QUAL_ONE: &'static str,
    const QUAL_MANY: &'static str,
    W,
> = QualConjList<QUAL_ONE, QUAL_MANY, "or", W>;

/// A list of values separated by a delimiter.
///
/// This is analogous to the typical `join` operation on lists.
/// Single-element lists will have no delimiter,
///   but lists of multiple elements will have each element delimited by the
///   same delimiter.
/// This is in contrast with wrappers that follow more sophisticated
///   typographical conventions,
///     such as [`ConjList`] and [`QualConjList`].
pub struct JoinListWrap<const DELIM: &'static str, W: DisplayWrapper>(
    PhantomData<W>,
);

impl<const DELIM: &'static str, W: DisplayWrapper> ListDisplayWrapper
    for JoinListWrap<DELIM, W>
{
    type Single = W;
    type First = W;
    type Middle = Prefix<DELIM, W>;
    type LastOfPair = Prefix<DELIM, W>;
    type LastOfMany = Prefix<DELIM, W>;
}

/// List wrapper with associated data.
///
/// This has the effect of creating an arbitrary [`Display`] implementation
///   for the wrapped list,
///     which will work well with [`format!`] and anything else that does
///     not have access to an explicit [`Formatter`].
pub struct ListWrap<'a, W: ListDisplayWrapper + ?Sized, T: Display> {
    list: &'a [T],
    _phantom: PhantomData<W>,
}

impl<'a, W: ListDisplayWrapper + ?Sized, T: Display> Display
    for ListWrap<'a, W, T>
{
    fn fmt(&self, f: &mut Formatter) -> Result {
        W::fmt(self.list, f)
    }
}

/// Wrap a `fmt`-like function to be used as [`Display::fmt`] for this
///   object.
///
/// This works around the problem of having a function expecting a
///   [`Formatter`],
///     but not having a [`Formatter`] to call it with.
/// It also allows for arbitrary (compatible) functions to be used as
///   [`Display`].
pub struct DisplayFn<F: Fn(&mut Formatter) -> Result>(pub F);

impl<F: Fn(&mut Formatter) -> Result> Display for DisplayFn<F> {
    fn fmt(&self, f: &mut Formatter) -> Result {
        (self.0)(f)
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn prefix() {
        assert_eq!(Prefix::<"@!", Raw>::wrap("foo").to_string(), "@!foo",);

        assert_eq!(
            Prefix::<"1", Prefix::<"2", Raw>>::wrap("bar").to_string(),
            "12bar",
        );
    }

    #[test]
    fn suffix() {
        assert_eq!(Suffix::<"!@", Raw>::wrap("foo").to_string(), "foo!@",);

        assert_eq!(
            Suffix::<"1", Suffix::<"2", Raw>>::wrap("bar").to_string(),
            "bar21",
        );
    }

    #[test]
    fn delimit() {
        assert_eq!(Delim::<"[", "]", Raw>::wrap("foo").to_string(), "[foo]",);

        assert_eq!(
            Delim::<"(", ")", Delim::<"|", "|", Raw>>::wrap("bar").to_string(),
            "(|bar|)",
        );
    }

    // Certain types are trivially verifiable by their definition,
    //   and so have no tests.

    #[test]
    fn conj_list_single() {
        assert_eq!(
            ConjList::<"unused", Raw>::wrap(&["single"]).to_string(),
            "single",
        );

        // Ensure that we're actually mapping.
        assert_eq!(
            ConjList::<"unused", Delim<"(", ")", Raw>>::wrap(&["single"])
                .to_string(),
            "(single)",
        );
    }

    #[test]
    fn conj_list_double() {
        assert_eq!(
            ConjList::<"and", Raw>::wrap(&["first", "second"]).to_string(),
            "first and second",
        );

        // Ensure that we're actually mapping.
        assert_eq!(
            ConjList::<"or", Delim<"(", ")", Raw>>::wrap(&["first", "second"])
                .to_string(),
            "(first) or (second)",
        );
    }

    #[test]
    fn conj_list_many() {
        assert_eq!(
            ConjList::<"and", Raw>::wrap(&["first", "second", "third"])
                .to_string(),
            "first, second, and third",
        );

        // Ensure that we're actually mapping.
        assert_eq!(
            ConjList::<"or", Delim<"[", "]", Raw>>::wrap(&[
                "first", "second", "third"
            ])
            .to_string(),
            "[first], [second], or [third]",
        );
    }

    #[test]
    fn qualified_conj_list_single() {
        assert_eq!(
            QualConjList::<"thing", "things", "unused", Raw>::wrap(&["a"])
                .to_string(),
            "thing a",
        );

        // Ensure that we're actually mapping.
        assert_eq!(
            QualConjList::<"thing", "things", "unused", Delim<"(", ")", Raw>>::wrap(&["a"])
                .to_string(),
            "thing (a)",
        );
    }

    #[test]
    fn qualified_conj_list_double() {
        assert_eq!(
            QualConjList::<"thing", "things", "and", Raw>::wrap(&["a", "b"])
                .to_string(),
            "things a and b",
        );

        // Ensure that we're actually mapping.
        assert_eq!(
            QualConjList::<"thing", "things", "or", Delim<"(", ")", Raw>>::wrap(&["a", "b"])
                .to_string(),
            "things (a) or (b)",
        );
    }

    #[test]
    fn qualified_conj_list_many() {
        assert_eq!(
            QualConjList::<"thing", "things", "and", Raw>::wrap(&[
                "a", "b", "c"
            ])
            .to_string(),
            "things a, b, and c",
        );

        // Ensure that we're actually mapping.
        assert_eq!(
            QualConjList::<"thing", "things", "or", Delim<"(", ")", Raw>>::wrap(&["a", "b", "c"])
                .to_string(),
            "things (a), (b), or (c)",
        );
    }

    #[test]
    fn display_fn() {
        assert_eq!(
            DisplayFn(|f| write!(f, "test fmt")).to_string(),
            "test fmt",
        );
    }

    // `fmt_nth` is used by the above tests,
    //   but that's an implementation detail;
    //     we expose it as a public API so it ought to be tested too.
    #[test]
    fn fmt_nth() {
        type Sut = QualConjList<"thing", "things", "or", Raw>;

        assert_eq!(
            DisplayFn(|f| Sut::fmt_nth(0, 0, &"foo", f)).to_string(),
            "thing foo",
        );

        assert_eq!(
            DisplayFn(|f| Sut::fmt_nth(1, 0, &"foo", f)).to_string(),
            "things foo",
        );

        assert_eq!(
            DisplayFn(|f| Sut::fmt_nth(1, 1, &"foo", f)).to_string(),
            " or foo",
        );

        assert_eq!(
            DisplayFn(|f| Sut::fmt_nth(2, 0, &"foo", f)).to_string(),
            "things foo",
        );

        assert_eq!(
            DisplayFn(|f| Sut::fmt_nth(2, 1, &"foo", f)).to_string(),
            ", foo",
        );

        assert_eq!(
            DisplayFn(|f| Sut::fmt_nth(2, 2, &"foo", f)).to_string(),
            ", or foo",
        );
    }

    #[test]
    fn delim_list() {
        assert_eq!(
            JoinListWrap::<"::", Raw>::wrap(&["one", "two", "three"])
                .to_string(),
            "one::two::three",
        );

        assert_eq!(
            JoinListWrap::<" -> ", TtQuote>::wrap(&["foo", "bar"]).to_string(),
            "`foo` -> `bar`",
        );
    }
}
