{-# LANGUAGE Safe #-}

{-|
Module      : Data.Char.Small
Description : A module used to render subscript and superscript in Unicode.
Maintainer  : hapytexeu+gh@gmail.com
Stability   : experimental
Portability : POSIX

One can make use of a <https://www.unicode.org/charts/PDF/U2070.pdf block of Unicode characters> to /emulate/ subscript and superscript. Note that the subscript and superscript will be
aligned with the /baseline/ and the /cap line/ respectively, and is thus not equivalent to @<sub>...</sub>@ and @<sup>...</sup>@ in HTML. Furthermore only a small subset of characters
is supported.

This module allows one to map certain characters to their subscript and superscript counterpart, and furthermore makes it more convenient to transform a number (both positive and negative)
to a 'Text' that specifies this number in subscript and superscript.
-}

module Data.Char.Small (
  -- * Convert characters to their subscript and superscript counterpart
    toSub, toSup
  -- * Numbers as subscript and superscript.
  , asSub, asSub', asSubPlus
  , asSup, asSup', asSupPlus
  -- * Ratio formatting
  , ratioToUnicode, ratioToUnicode'
  ) where

import Data.Char(chr, isDigit, ord)
import Data.Char.Core(PlusStyle(WithPlus, WithoutPlus), positionalNumberSystem10)
import Data.Default(Default(def))
import Data.Ratio(Ratio, denominator, numerator)
import Data.Text(Text, cons, snoc, singleton)

-- | Convert a set of characters to their superscript counterpart, given that
-- characters exists.
toSup
    :: Char  -- ^ The given character to convert to its superscript counterpart.
    -> Maybe Char -- ^ A character wrapped in a 'Just' given the counterpart exists, 'Nothing' otherwise.
toSup 'i' = Just '\x2071'
toSup '+' = Just '\x207a'
toSup '-' = Just '\x207b'
toSup '\x2212' = Just '\x207b'
toSup '=' = Just '\x207c'
toSup '(' = Just '\x207d'
toSup ')' = Just '\x207e'
toSup 'n' = Just '\x207f'
toSup c | isDigit c = Just (_digitToSub (ord c - ord '0'))
        | otherwise = Nothing

-- | Convert a set of characters to their subscript counterpart, given that
-- characters exists.
toSub
    :: Char  -- ^ The given character to convert to its subscript counterpart.
    -> Maybe Char -- ^ A character wrapped in a 'Just' given the counterpart exists, 'Nothing' otherwise.
toSub '+' = Just '\x208a'
toSub '-' = Just '\x208b'
toSub '\x2212' = Just '\x208b'
toSub '=' = Just '\x208c'
toSub '(' = Just '\x208d'
toSub ')' = Just '\x208e'
toSub 'a' = Just '\x2090'
toSub 'e' = Just '\x2091'
toSub 'o' = Just '\x2092'
toSub 'x' = Just '\x2093'
toSub '\x259' = Just '\x2094'
toSub 'h' = Just '\x2095'
toSub 'k' = Just '\x2095'
toSub 'l' = Just '\x2095'
toSub 'm' = Just '\x2095'
toSub 'n' = Just '\x2095'
toSub 'p' = Just '\x2095'
toSub 's' = Just '\x2095'
toSub 't' = Just '\x2095'
toSub c | isDigit c = Just (_digitToSub (ord c - ord '0'))
        | otherwise = Nothing

_value :: Integral i => (Int -> Char) -> i -> Text
_value f = go
    where f' = f . fromIntegral
          go n | n <= 9 = singleton (f' n)
               | otherwise = snoc (go q) (f' r)
               where (q,r) = quotRem n 10

_prefixSign :: Integral i => Char -> (Int -> Char) -> i -> Text
_prefixSign c f v
  | v < 0 = cons c (f' (-v))
  | otherwise = f' v
  where f' = _value f

_prefixSignPlus :: Integral i => Char -> Char -> (Int -> Char) -> i -> Text
_prefixSignPlus cp cn f v
  | v < 0 = c' cn (-v)
  | otherwise = c' cp v
  where c' = (. _value f) . cons

-- | Convert the given 'Ratio' object to a sequence of characters with the
-- numerator in superscript and the denominator in subscript. The given
-- 'PlusStyle' is applied to the numerator.
ratioToUnicode :: Integral i
  => PlusStyle -- ^ The given 'PlusStyle' to use.
  -> Ratio i -- ^ The given 'Ratio' object to convert to a 'Text'.
  -> Text -- ^ A 'Text' object that denotes the given 'Ratio' making use of superscript and subscript.
ratioToUnicode ps dn = asSup ps (numerator dn) <> cons '\x2044' (asSub' (denominator dn))

-- | Format a given 'Ratio' object to a 'Text' value that formats the ratio with
-- superscript and subscript using the 'Default' 'PlusStyle'.
ratioToUnicode' :: Integral i
    => Ratio i -- ^ The given 'Ratio' value to format.
    -> Text -- ^ The 'Text' block that contains a textual representation of the 'Ratio'.
ratioToUnicode' = ratioToUnicode def

-- | Convert a number (positive or negative) to a 'Text' object that denotes
-- that number in superscript characters.
asSup :: Integral i
  => PlusStyle -- ^ The given 'PlusStyle' to use.
  -> i -- ^ The given number to convert.
  -> Text -- ^ A 'Text' value that denotes the number as a sequence of superscript characters.
asSup = positionalNumberSystem10 _digitToSup '\x207a' '\x207b'

-- | Convert a number (positive or negative) to a 'Text' object that denotes that
-- number in superscript characters.
asSup' :: Integral i
    => i -- ^ The number to convert.
    -> Text -- ^ A 'Text' value that contains the number as a sequence of superscript characters.
asSup' = asSup WithoutPlus

-- | Convert a number (positive or negative) to a 'Text' that specifies that
-- number in superscript characters. For positive characters, the superscript
-- contains a plus character (@⁺@).
asSupPlus :: Integral i
    => i -- ^ The number to convert.
    -> Text -- ^ A 'Text' value that contains the number as a sequence of superscript characters.
asSupPlus = asSup WithPlus -- _prefixSignPlus '\x207a' '\x207b' _digitToSup

-- | Convert a number (positive or negative) to a 'Text' object that denotes
-- that number in subscript characters.
asSub :: Integral i
  => PlusStyle -- ^ The given 'PlusStyle' to use.
  -> i -- ^ The given number to convert.
  -> Text -- ^ A 'Text' value that denotes the number as a sequence of subscript characters.
asSub = positionalNumberSystem10 _digitToSub '\x208a' '\x208b'

-- | Convert a number (positive or negative) to a 'Text' that specifies that
-- number in subscript characters.
asSub' :: Integral i
    => i -- ^ The number to convert.
    -> Text -- ^ A 'Text' value that contains the number as a sequence of subscript characters.
asSub' = asSub WithoutPlus

-- | Convert a number (positive or negative) to a 'Text' that specifies that
-- number in subscript characters. For positive characters, the subscript
-- contains a plus character (@₊@).
asSubPlus :: Integral i
    => i -- ^ The number to convert.
    -> Text -- ^ A 'Text' value that contains the number as a sequence of subscript characters.
asSubPlus = asSub WithPlus

_digitToSub :: Int -> Char
_digitToSub = chr . (8320+)

_digitToSup :: Int -> Char
_digitToSup 0 = '\x2070'
_digitToSup 1 = '\xb9'
_digitToSup n | n <= 3 = chr (176+n)
              | otherwise = chr (8304+n)