//! Convert TrueType and OpenType (SFNT) fonts into the legacy Internet
//! Explorer EOT (Embedded OpenType) format.
//!
//! The crate exposes one function, [`ttf2eot`]. It reads the `OS/2`, `head`,
//! and `name` tables from an SFNT font, builds an 82-byte EOT header, appends
//! four name strings as UTF-16LE, then appends the input font verbatim. It does
//! no compression and no glyph processing.
//!
//! SFNT input is big-endian. EOT output is little-endian. The function copies
//! the font payload byte for byte.
//!
//! # Example
//!
//! ```no_run
//! let ttf = std::fs::read("font.ttf").unwrap();
//! let eot = ttf2eot::ttf2eot(&ttf).expect("valid SFNT font");
//! std::fs::write("font.eot", eot).unwrap();
//! ```
//!
//! # Errors
//!
//! [`ttf2eot`] returns [`Error`] when a required table is missing or when the
//! input is too short to read a structure the algorithm needs.

#![forbid(unsafe_code)]
#![warn(missing_docs)]

mod offsets;
mod reader;

use offsets::{eot, magic, sfnt, sizeof};
use reader::Reader;

/// Failure modes of [`ttf2eot`].
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Error {
    /// One of the required tables (`OS/2`, `head`, `name`) was not found in the
    /// table directory.
    RequiredTableNotFound,
    /// A read ran past the end of the input. The input is truncated or a table
    /// offset or length points outside the font.
    UnexpectedEof,
}

impl core::fmt::Display for Error {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            Error::RequiredTableNotFound => f.write_str("required table not found"),
            Error::UnexpectedEof => f.write_str("unexpected end of input"),
        }
    }
}

impl std::error::Error for Error {}

/// Convert an SFNT (TTF/OTF) font into an EOT font.
///
/// `input` holds the raw font bytes. The return value is a fresh EOT buffer.
/// The input is read only and never modified.
///
/// # Output layout
///
/// ```text
/// [ 82-byte EOT prefix header                  ]
/// [ familyName    : u16 len + UTF-16LE + 0x0000 ]
/// [ subfamilyName : u16 len + UTF-16LE + 0x0000 ]
/// [ versionString : u16 len + UTF-16LE + 0x0000 ]
/// [ fullName      : u16 len + UTF-16LE + 0x0000 ]
/// [ 0x00 0x00 pad                               ]
/// [ original font bytes, verbatim               ]
/// ```
///
/// The strings appear in the order family, subfamily, version, full. Version
/// comes before full. A name string with no matching record contributes a
/// single `0x00` byte.
///
/// # Errors
///
/// Returns [`Error::RequiredTableNotFound`] when the `OS/2`, `head`, or
/// `name` table is absent. Returns [`Error::UnexpectedEof`] when a read would
/// pass the end of the input.
pub fn ttf2eot(input: &[u8]) -> Result<Vec<u8>, Error> {
    let font = Reader::new(input);

    let mut out = vec![0u8; sizeof::EOT_PREFIX];
    write_u32_le(&mut out, eot::FONT_LENGTH, input.len() as u32);
    write_u32_le(&mut out, eot::VERSION, magic::EOT_VERSION);
    out[eot::CHARSET] = magic::EOT_CHARSET;
    write_u16_le(&mut out, eot::MAGIC, magic::EOT_MAGIC);

    // Missing name strings each contribute one zero byte, not an empty strbuf.
    let mut family_name = vec![0u8];
    let mut subfamily_name = vec![0u8];
    let mut full_name = vec![0u8];
    let mut version_string = vec![0u8];

    let mut have_os2 = false;
    let mut have_name = false;
    let mut have_head = false;

    let num_tables = font.u16_be(sfnt::NUMTABLES)?;

    for i in 0..num_tables as usize {
        let entry = font.slice_from(sizeof::SFNT_HEADER + i * sizeof::SFNT_TABLE_ENTRY);
        let tag = entry.bytes(sfnt::TABLE_TAG, 4)?;
        let offset = entry.u32_be(sfnt::TABLE_OFFSET)? as usize;
        let length = entry.u32_be(sfnt::TABLE_LENGTH)? as usize;
        let table = font.slice(offset, length);

        if tag == b"OS/2" {
            have_os2 = true;
            let os2_version = table.u16_be(0)?;

            for j in 0..10 {
                out[eot::FONT_PANOSE + j] = table.u8(sfnt::OS2_FONT_PANOSE + j)?;
            }

            out[eot::ITALIC] = (table.u16_be(sfnt::OS2_FS_SELECTION)? & 0x01) as u8;
            write_u32_le(
                &mut out,
                eot::WEIGHT,
                table.u16_be(sfnt::OS2_WEIGHT)? as u32,
            );

            for j in 0..4 {
                let v = table.u32_be(sfnt::OS2_UNICODE_RANGE + j * 4)?;
                write_u32_le(&mut out, eot::UNICODE_RANGE + j * 4, v);
            }

            // Codepage ranges exist only in OS/2 version 1 and later. Reading
            // them from a version 0 table would run past its end.
            if os2_version >= 1 {
                for j in 0..2 {
                    let v = table.u32_be(sfnt::OS2_CODEPAGE_RANGE + j * 4)?;
                    write_u32_le(&mut out, eot::CODEPAGE_RANGE + j * 4, v);
                }
            }
        } else if tag == b"head" {
            have_head = true;
            let v = table.u32_be(sfnt::HEAD_CHECKSUM_ADJUSTMENT)?;
            write_u32_le(&mut out, eot::CHECKSUM_ADJUSTMENT, v);
        } else if tag == b"name" {
            have_name = true;
            let count = table.u16_be(sfnt::NAMETABLE_COUNT)?;
            let string_offset = table.u16_be(sfnt::NAMETABLE_STRING_OFFSET)? as usize;

            for j in 0..count as usize {
                let rec =
                    table.slice_from(sizeof::SFNT_NAMETABLE + j * sizeof::SFNT_NAMETABLE_ENTRY);
                let platform_id = rec.u16_be(sfnt::NAME_PLATFORM_ID)?;
                let encoding_id = rec.u16_be(sfnt::NAME_ENCODING_ID)?;
                let language_id = rec.u16_be(sfnt::NAME_LANGUAGE_ID)?;
                let name_id = rec.u16_be(sfnt::NAME_NAME_ID)?;
                let len = rec.u16_be(sfnt::NAME_LENGTH)? as usize;
                let off = rec.u16_be(sfnt::NAME_OFFSET)? as usize;

                if platform_id == 3 && encoding_id == 1 && language_id == magic::LANGUAGE_ENGLISH {
                    // The string slice clamps to the end of the name table. A
                    // record whose length runs past the table yields a shorter
                    // string, and the conversion continues.
                    let raw = table.clamped(string_offset + off, len);
                    let s = strbuf(raw);
                    // Later records for the same nameID overwrite earlier ones.
                    match name_id {
                        1 => family_name = s,
                        2 => subfamily_name = s,
                        4 => full_name = s,
                        5 => version_string = s,
                        _ => {}
                    }
                }
            }
        }

        if have_os2 && have_name && have_head {
            break;
        }
    }

    if !(have_os2 && have_name && have_head) {
        return Err(Error::RequiredTableNotFound);
    }

    // Order is family, subfamily, version, full. Version comes before full.
    let mut eot_buf = Vec::with_capacity(
        out.len()
            + family_name.len()
            + subfamily_name.len()
            + version_string.len()
            + full_name.len()
            + 2
            + input.len(),
    );
    eot_buf.extend_from_slice(&out);
    eot_buf.extend_from_slice(&family_name);
    eot_buf.extend_from_slice(&subfamily_name);
    eot_buf.extend_from_slice(&version_string);
    eot_buf.extend_from_slice(&full_name);
    eot_buf.extend_from_slice(&[0, 0]);
    eot_buf.extend_from_slice(input);

    let total = eot_buf.len() as u32;
    write_u32_le(&mut eot_buf, eot::LENGTH, total);

    Ok(eot_buf)
}

/// Convert a UTF-16BE byte slice into the EOT string encoding.
///
/// The result has length `str.len() + 4`. The first two bytes hold the input
/// byte length as a little-endian `u16`. The middle holds the string with each
/// 16-bit code unit byte-swapped from big-endian to little-endian. The last two
/// bytes are a `0x0000` terminator.
///
/// The loop steps by two bytes. Valid `name` strings are UTF-16 and have even
/// length, so this consumes every byte. An odd trailing byte is left out, which
/// matches valid input.
fn strbuf(s: &[u8]) -> Vec<u8> {
    let mut out = vec![0u8; s.len() + 4];
    write_u16_le(&mut out, 0, s.len() as u16);

    let mut i = 0;
    while i + 1 < s.len() {
        let unit = u16::from_be_bytes([s[i], s[i + 1]]);
        write_u16_le(&mut out, i + 2, unit);
        i += 2;
    }
    // The terminator at the end is already zero.
    out
}

fn write_u16_le(buf: &mut [u8], offset: usize, value: u16) {
    buf[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
}

fn write_u32_le(buf: &mut [u8], offset: usize, value: u32) {
    buf[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
}
