packages feed

bidi-icu-0: src/Data/Text/ICU/Bidi.hsc

{-# language TemplateHaskell #-}
{-# language QuasiQuotes #-}
{-# language ViewPatterns #-}
{-# language OverloadedStrings #-}
{-# language TupleSections #-}
{-# language DeriveDataTypeable #-}
{-# language DeriveGeneric #-}
{-# language PatternSynonyms #-}
{-# language LambdaCase #-}
{-# language ScopedTypeVariables #-}
{-# language GeneralizedNewtypeDeriving #-}
{-# language TypeApplications #-}
{-# language BangPatterns #-}
{-# language MagicHash #-} -- has fun interactions with hsc2hs!
{-# language PolyKinds #-}
{-# language DataKinds #-}
{-# language UnboxedTuples #-}
{-# language BlockArguments #-}
{-# options_ghc -Wno-missing-pattern-synonym-signatures #-}

module Data.Text.ICU.Bidi
( Bidi(..)
, pattern MAP_NOWHERE

, open
, openSized

, countParagraphs
, countRuns

, getCustomizedClass
, getLength
, getLevelAt
, getLevels
, getLogicalIndex
, getLogicalMap
, getLogicalRun
, getParaLevel
, getParagraph
, getParagraphByIndex
, getProcessedLength
, getResultLength
, getText
, getVisualIndex
, getVisualMap
, getVisualRun
, invertMap
, isInverse
, isOrderParagraphsLTR
, orderParagraphsLTR
, reorderLogical
, reorderVisual
, setContext
, setInverse
, setLine
, setPara
-- * Levels
, Level
  ( Level
  , DEFAULT_LTR
  , DEFAULT_RTL
  , MAX_EXPLICIT_LEVEL
  )
, isRTL, isLTR
, isOverride
, override
, pattern LEVEL_OVERRIDE
-- * Direction
, Direction(..)
, getBaseDirection
, getDirection
-- * Reordering
, ReorderingMode(..)
, getReorderingMode
, setReorderingMode
, ReorderingOption
  ( ReorderingOption
  , OPTION_DEFAULT
  , OPTION_INSERT_MARKS
  , OPTION_REMOVE_CONTROLS
  , OPTION_STREAMING
  )
, getReorderingOptions
, setReorderingOptions
-- * Character Direction Classes
, CharDirection
  ( CharDirection
  , LEFT_TO_RIGHT
  , RIGHT_TO_LEFT
  , EUROPEAN_NUMBER
  , EUROPEAN_NUMBER_SEPARATOR
  , EUROPEAN_NUMBER_TERMINATOR
  , ARABIC_NUMBER
  , COMMON_NUMBER_SEPARATOR
  , BLOCK_SEPARATOR
  , SEGMENT_SEPARATOR
  , WHITE_SPACE_NEUTRAL
  , OTHER_NEUTRAL
  , LEFT_TO_RIGHT_EMBEDDING
  , LEFT_TO_RIGHT_OVERRIDE
  , RIGHT_TO_LEFT_ARABIC
  , RIGHT_TO_LEFT_EMBEDDING
  , RIGHT_TO_LEFT_OVERRIDE
  , POP_DIRECTIONAL_FORMAT
  , DIR_NON_SPACING_MARK
  , BOUNDARY_NEUTRAL
  , FIRST_STRONG_ISOLATE
  , LEFT_TO_RIGHT_ISOLATE
  , RIGHT_TO_LEFT_ISOLATE
  , POP_DIRECTIONAL_ISOLATE
  , BIDI_CLASS_DEFAULT -- hack
  )

, ClassCallback
, mkClassCallback
, setClassCallback
, getClassCallback

-- * Writing
, WriteOptions
  ( WriteOptions
  , DO_MIRRORING
  , INSERT_LRM_FOR_NUMERIC
  , KEEP_BASE_COMBINING
  , REMOVE_BIDI_CONTROLS
  , OUTPUT_REVERSE
  )
, writeReordered
, writeReverse

-- * Internal
, UBiDi
, UErrorCode(..)
) where

import Control.Exception
import Control.Monad
import Control.Monad.Primitive
import Data.Bits
import Data.Coerce
import Data.Data (Data)
import Data.Default
import Data.Functor ((<&>))
import Data.Int
import Data.IORef
import qualified Data.Map as Map
import Data.Maybe (fromMaybe)
import Data.Primitive.ByteArray
import Data.Primitive.PrimArray
import Data.Primitive.Ptr as Prim
import Data.Primitive.Types
import Data.Text as Text
import Data.Text.Foreign as Text
import Data.Traversable (for)
import qualified Data.Vector.Primitive as Prim
import Data.Word
import Foreign.C.String
import Foreign.C.Types
import qualified Foreign.Concurrent as Concurrent
import Foreign.ForeignPtr
import Foreign.Marshal.Alloc
import Foreign.Marshal.Array
import Foreign.Marshal.Unsafe (unsafeLocalState)
import Foreign.Marshal.Utils
import Foreign.Ptr
import Foreign.Storable
import GHC.Arr (Ix)
import GHC.Generics (Generic)
import GHC.Types
import qualified Language.C.Inline as C
import qualified Language.C.Inline.Context as C
import qualified Language.C.Inline.HaskellIdentifier as C
import qualified Language.C.Types as C
import qualified Language.Haskell.TH as TH
import System.IO.Unsafe (unsafePerformIO)

--------------------------------------------------------------------------------
-- PrimArray utilities
--------------------------------------------------------------------------------

withPrimArrayLen :: forall a r. Prim a => PrimArray a -> (Int -> Ptr a -> IO r) -> IO r
withPrimArrayLen pa k = allocaBytes (n * I## (sizeOf## @a undefined)) \p -> copyPrimArrayToPtr p pa 0 n *> k n p where
  n = sizeofPrimArray pa

peekPrimArray :: Prim a => Int -> Ptr a -> IO (PrimArray a)
peekPrimArray len ptr = do
  mpa <- newPrimArray len
  copyPtrToMutablePrimArray mpa 0 ptr len
  unsafeFreezePrimArray mpa

#ifndef HLINT
#include "unicode/utypes.h"
#include "unicode/uchar.h"
#include "unicode/localpointer.h"
#include "unicode/ubidi.h"
#endif

newtype Level = Level Word8
  deriving (Eq,Ord,Show,Storable,Prim)

isRTL :: Level -> Bool
isRTL = coerce (odd @Word8)

isLTR :: Level -> Bool
isLTR = coerce (even @Word8)

pattern LEVEL_OVERRIDE = (#const UBIDI_LEVEL_OVERRIDE) :: Word8

isOverride :: Level -> Bool
isOverride (Level l) = l .&. LEVEL_OVERRIDE /= 0

override :: Level -> Level
override (Level l) = Level (l .|. LEVEL_OVERRIDE)

#ifndef HLINT
pattern DEFAULT_LTR = Level (#const UBIDI_DEFAULT_LTR)
pattern DEFAULT_RTL = Level (#const UBIDI_DEFAULT_RTL)
pattern MAX_EXPLICIT_LEVEL = Level (#const UBIDI_MAX_EXPLICIT_LEVEL)
#endif

#ifndef HLINT
-- | Special value which can be returned by the mapping functions when a logical index has no corresponding visual index or vice-versa.
-- Returned by 'getVisualIndex', 'getVisualMap', 'getLogicalIndex', 'getLogicalMap'
pattern MAP_NOWHERE = (#const UBIDI_MAP_NOWHERE) :: Int
#endif

newtype WriteOptions = WriteOptions Int16
  deriving (Eq,Ord,Show,Storable,Prim,Bits)

#ifndef HLINT
pattern KEEP_BASE_COMBINING = WriteOptions (#const UBIDI_KEEP_BASE_COMBINING)
pattern DO_MIRRORING = WriteOptions (#const UBIDI_DO_MIRRORING)
pattern INSERT_LRM_FOR_NUMERIC = WriteOptions (#const UBIDI_INSERT_LRM_FOR_NUMERIC)
pattern REMOVE_BIDI_CONTROLS  = WriteOptions (#const UBIDI_REMOVE_BIDI_CONTROLS )
pattern OUTPUT_REVERSE = WriteOptions (#const UBIDI_OUTPUT_REVERSE)
#endif

instance Default WriteOptions where
  def = WriteOptions 0

newtype UErrorCode = UErrorCode Int32
  deriving (Eq,Ord,Show,Num,Enum,Real,Integral,Storable)

instance Default UErrorCode where
  def = UErrorCode 0

-- |
-- 'ReorderingOption' values indicate which options are
-- specified to affect the Bidi algorithm.
newtype ReorderingOption = ReorderingOption Int32
  deriving (Eq,Ord,Show,Bits)

#ifndef HLINT
-- | 
-- option for 'setReorderingOptions' that disables all the options which can be
-- set with this function
-- 
-- @since ICU 3.6
pattern OPTION_DEFAULT = ReorderingOption (#const UBIDI_OPTION_DEFAULT)
-- | @since ICU 3.6
pattern OPTION_INSERT_MARKS = ReorderingOption (#const UBIDI_OPTION_INSERT_MARKS)
-- | @since ICU 3.6
pattern OPTION_REMOVE_CONTROLS = ReorderingOption (#const UBIDI_OPTION_REMOVE_CONTROLS)
-- | 
pattern OPTION_STREAMING = ReorderingOption (#const UBIDI_OPTION_STREAMING)
#endif

instance Default ReorderingOption where
  def = OPTION_DEFAULT

-- | Character Directions.
--
-- This is morally the same as text-icu's Direction type, but that one is missing a few definitions =(
--
-- When issue <https://github.com/haskell/text-icu/issues/44 44> is resolved, this will
-- be able to be text.icu's @Data.Text.ICU.Char.Direction@.

newtype CharDirection = CharDirection Int32 deriving
  (Eq,Ord,Show,Storable,Prim)

#ifndef HLINT
-- | L @\@stable@ ICU 2.0
pattern LEFT_TO_RIGHT = CharDirection (#const U_LEFT_TO_RIGHT)
-- | R @\@stable@ ICU 2.0
pattern RIGHT_TO_LEFT = CharDirection (#const U_RIGHT_TO_LEFT)
-- | EN @\@stable@ ICU 2.0
pattern EUROPEAN_NUMBER = CharDirection (#const U_EUROPEAN_NUMBER)
-- | ES @\@stable@ ICU 2.0
pattern EUROPEAN_NUMBER_SEPARATOR = CharDirection (#const U_EUROPEAN_NUMBER_SEPARATOR)
-- | ET @\@stable@ ICU 2.0
pattern EUROPEAN_NUMBER_TERMINATOR = CharDirection (#const U_EUROPEAN_NUMBER_TERMINATOR)
-- | AN @\@stable@ ICU 2.0
pattern ARABIC_NUMBER = CharDirection (#const U_ARABIC_NUMBER)
-- | CS @\@stable@ ICU 2.0
pattern COMMON_NUMBER_SEPARATOR = CharDirection (#const U_COMMON_NUMBER_SEPARATOR)
-- | B @\@stable@ ICU 2.0
pattern BLOCK_SEPARATOR = CharDirection (#const U_BLOCK_SEPARATOR)
-- | SS@\@stable@ ICU 2.0
pattern SEGMENT_SEPARATOR = CharDirection (#const U_SEGMENT_SEPARATOR)
-- | WS @\@stable@ ICU 2.0
pattern WHITE_SPACE_NEUTRAL = CharDirection (#const U_WHITE_SPACE_NEUTRAL)
-- | ON @\@stable@ ICU 2.0
pattern OTHER_NEUTRAL = CharDirection (#const U_OTHER_NEUTRAL)
-- | LRE @\@stable@ ICU 2.0
pattern LEFT_TO_RIGHT_EMBEDDING = CharDirection (#const U_LEFT_TO_RIGHT_EMBEDDING)
-- | LRO @\@stable@ ICU 2.0
pattern LEFT_TO_RIGHT_OVERRIDE = CharDirection (#const U_LEFT_TO_RIGHT_OVERRIDE)
-- | AL @\@stable@ ICU 2.0
pattern RIGHT_TO_LEFT_ARABIC = CharDirection (#const U_RIGHT_TO_LEFT_ARABIC)
-- | RLE @\@stable@ ICU 2.0
pattern RIGHT_TO_LEFT_EMBEDDING = CharDirection (#const U_RIGHT_TO_LEFT_EMBEDDING)
-- | RLO @\@stable@ ICU 2.0
pattern RIGHT_TO_LEFT_OVERRIDE = CharDirection (#const U_RIGHT_TO_LEFT_OVERRIDE)
-- | PDF @\@stable@ ICU 2.0
pattern POP_DIRECTIONAL_FORMAT = CharDirection (#const U_POP_DIRECTIONAL_FORMAT)
-- | NSM @\@stable@ ICU 2.0
pattern DIR_NON_SPACING_MARK = CharDirection (#const U_DIR_NON_SPACING_MARK)

-- after text-icu scanned the headers

-- | BN @\@stable@ ICU 52
pattern BOUNDARY_NEUTRAL = CharDirection (#const U_BOUNDARY_NEUTRAL)
-- | FSI @\@stable@ ICU 52
pattern FIRST_STRONG_ISOLATE = CharDirection (#const U_FIRST_STRONG_ISOLATE)
-- | LRI @\@stable@ ICU 52
pattern LEFT_TO_RIGHT_ISOLATE = CharDirection (#const U_LEFT_TO_RIGHT_ISOLATE)
-- | RLI @\@stable@ ICU 52
pattern RIGHT_TO_LEFT_ISOLATE = CharDirection (#const U_RIGHT_TO_LEFT_ISOLATE)
-- | PDI @\@stable@ ICU 52
pattern POP_DIRECTIONAL_ISOLATE = CharDirection (#const U_POP_DIRECTIONAL_ISOLATE)

-- | ICU 58 The numeric value may change over time, see ICU ticket #12420.
pattern BIDI_CLASS_DEFAULT = CharDirection (#const U_BIDI_CLASS_DEFAULT) -- a damn lie
#endif

type ClassCallback = Ptr () -> Int32 -> IO CharDirection

foreign import ccall "wrapper" mkClassCallback :: ClassCallback -> IO (FunPtr ClassCallback)

ubool :: Int8 -> Bool
ubool = (0/=)

boolu :: Bool -> Int8
boolu = fromIntegral . fromEnum

data Direction
  = LTR
  | RTL
  | Mixed
  | Neutral
  deriving (Eq,Ord,Show,Ix,Enum,Bounded,Data,Generic)

data ReorderingMode
  = ReorderDefault
  | ReorderNumbersSpecial
  | ReorderGroupNumbersWithR
  | ReorderRunsOnly
  | ReorderInverseNumbersAsL
  | ReorderInverseLikeDirect
  | ReorderInverseForNumbersSpecial
  | ReorderCount
  deriving (Eq,Ord,Show,Ix,Enum,Bounded,Data,Generic)

data UBiDi

data Bidi s = Bidi
  { embeddingLevels :: IORef (Ptr Level)  -- used to deal with ubidi_setPara shared content issues
  , parentLink :: IORef (Maybe (Bidi s))  -- used to deal with ubidi_setLine shared content issues
  , getBidi :: ForeignPtr UBiDi
  }

withBidi :: Bidi s -> (Ptr UBiDi -> IO r) -> IO r
withBidi = withForeignPtr . getBidi

let
  anti cTy hsTyQ w = C.SomeAntiQuoter C.AntiQuoter
    { C.aqParser = C.parseIdentifier <&> \hId -> (C.mangleHaskellIdentifier False hId, cTy, hId)
    , C.aqMarshaller = \_ _ _ cId -> (,) <$> hsTyQ <*> [|$w (coerce $(getHsVariable "bidirectionalCtx" cId))|]
    }
  getHsVariable err s = TH.lookupValueName (C.unHaskellIdentifier s) >>= \ case
    Nothing -> fail $ "Cannot capture Haskell variable " ++ C.unHaskellIdentifier s ++ ", because it's not in scope. (" ++ err ++ ")"
    Just hsName -> TH.varE hsName
 in C.context $ C.baseCtx <> C.fptrCtx <> mempty
      { C.ctxTypesTable = Map.fromList
        [ (C.TypeName "UBiDi", [t|UBiDi|])
        , (C.TypeName "UBiDiDirection", [t|Int32|])
        , (C.TypeName "UBiDiLevel", [t|Level|])
        , (C.TypeName "UBiDiReorderingMode", [t|Int32|])
        , (C.TypeName "UBiDiReorderingOption", [t|ReorderingOption|])
        , (C.TypeName "UBiDiClassCallbackPtr", [t|FunPtr ClassCallback|])
        , (C.TypeName "UBool", [t|Int8|])
        , (C.TypeName "UChar", [t|Word16|])
        , (C.TypeName "UChar32", [t|Int32|])
        , (C.TypeName "UCharDirection", [t|CharDirection|])
        , (C.TypeName "UErrorCode", [t|UErrorCode|])
        , (C.TypeName "WriteOptions", [t|WriteOptions|])
        ]
      , C.ctxAntiQuoters = Map.fromList
        [ ("bidi", anti (C.Ptr [] $ C.TypeSpecifier mempty $ C.TypeName "UBiDi") 
                        [t|Ptr UBiDi|] [|withBidi|]
          )
        ]
      }

C.include "HsFFI.h"
C.include "unicode/utypes.h"
C.include "unicode/uchar.h"
C.include "unicode/localpointer.h"
C.include "unicode/ubidi.h"

C.verbatim "typedef UBiDiClassCallback * UBiDiClassCallbackPtr;"
C.verbatim "typedef int16_t WriteOptions;"

instance Exception UErrorCode where
  displayException e = unsafeLocalState $ peekCString 
    [C.pure|const char * { u_errorName($(UErrorCode e)) }|]

foreignBidi :: Ptr UBiDi -> IO (Bidi s)
foreignBidi self_ptr = do
  embeddings_ref <- newIORef nullPtr -- embeddingLevels
  parent_ref <- newIORef Nothing -- parentLink
  self_fptr <- Concurrent.newForeignPtr self_ptr do
    [C.block|void { ubidi_close($(UBiDi * self_ptr)); }|] -- delete self
    embeddings <- readIORef embeddings_ref -- clean up embeddings
    when (embeddings /= nullPtr) do free embeddings
    -- garbage collecting the parent link will allow parent to now possibly be freed if it has no references
  pure $ Bidi embeddings_ref parent_ref self_fptr

bad :: UErrorCode -> Bool
bad e = [C.pure|int { U_FAILURE($(UErrorCode e)) }|] /= 0

ok :: UErrorCode -> IO ()
ok e = when (bad e) do throwIO e

open :: PrimMonad m => m (Bidi (PrimState m))
open = unsafeIOToPrim do [C.exp|UBiDi * { ubidi_open() }|] >>= foreignBidi

openSized :: PrimMonad m => Int32 -> Int32 -> m (Bidi (PrimState m))
openSized maxLength maxRunCount = unsafeIOToPrim do
  with def \pErrorCode -> do
    bidi <- [C.exp|UBiDi * { ubidi_openSized($(int32_t maxLength),$(int32_t maxRunCount),$(UErrorCode * pErrorCode)) }|]
    peek pErrorCode >>= ok
    foreignBidi bidi

getText :: PrimMonad m => Bidi (PrimState m) -> m Text
getText bidi = unsafeIOToPrim do
  withBidi bidi \p -> do
    cwstr <- [C.exp|const UChar * { ubidi_getText($(const UBiDi * p))}|]
    len <- [C.exp|int32_t { ubidi_getLength($(const UBiDi * p))}|]
    fromPtr cwstr (fromIntegral len)

getLength :: PrimMonad m => Bidi (PrimState m) -> m Int32
getLength bidi = unsafeIOToPrim [C.exp|int32_t { ubidi_getLength($bidi:bidi) }|]

setInverse :: PrimMonad m => Bidi (PrimState m) -> Bool -> m ()
setInverse bidi (boolu -> b) = unsafeIOToPrim [C.block|void { ubidi_setInverse($bidi:bidi,$(UBool b)); }|]

isInverse :: PrimMonad m => Bidi (PrimState m) -> m Bool
isInverse bidi = unsafeIOToPrim do [C.exp|UBool { ubidi_isInverse($bidi:bidi) }|] <&> ubool

orderParagraphsLTR :: PrimMonad m => Bidi (PrimState m) -> Bool -> m ()
orderParagraphsLTR bidi (boolu -> b) = unsafeIOToPrim [C.block|void { ubidi_orderParagraphsLTR($bidi:bidi,$(UBool b)); }|]

isOrderParagraphsLTR :: PrimMonad m => Bidi (PrimState m) -> m Bool
isOrderParagraphsLTR bidi = unsafeIOToPrim do [C.exp|UBool { ubidi_isOrderParagraphsLTR($bidi:bidi) }|] <&> ubool

setReorderingMode :: PrimMonad m => Bidi (PrimState m) -> ReorderingMode -> m ()
setReorderingMode bidi (fromIntegral . fromEnum -> mode) = unsafeIOToPrim [C.block|void { ubidi_setReorderingMode($bidi:bidi,$(UBiDiReorderingMode mode)); }|]

getReorderingMode :: PrimMonad m => Bidi (PrimState m) -> m ReorderingMode
getReorderingMode bidi = unsafeIOToPrim do [C.exp|UBiDiReorderingMode{ ubidi_getReorderingMode($bidi:bidi)}|] <&> toEnum . fromIntegral

setReorderingOptions :: PrimMonad m => Bidi (PrimState m) -> ReorderingOption -> m ()
setReorderingOptions bidi options = unsafeIOToPrim [C.block|void { ubidi_setReorderingOptions($bidi:bidi,$(UBiDiReorderingOption options)); }|]

getReorderingOptions :: PrimMonad m => Bidi (PrimState m) -> m ReorderingOption
getReorderingOptions bidi = unsafeIOToPrim do [C.exp|UBiDiReorderingOption { ubidi_getReorderingOptions($bidi:bidi) }|]

setContext :: PrimMonad m => Bidi (PrimState m) -> Text -> Text -> m ()
setContext bidi prologue_text epilogue_text = unsafeIOToPrim do
  useAsPtr prologue_text \prologue (fromIntegral -> prologue_len) ->
    useAsPtr epilogue_text \epilogue (fromIntegral -> epilogue_len) ->
      [C.block|UErrorCode {
        UErrorCode error_code = 0;
        ubidi_setContext(
          $bidi:bidi,
          $(const UChar * prologue),
          $(int32_t prologue_len),
          $(const UChar * epilogue),
          $(int32_t epilogue_len),
          &error_code
        );
        return error_code;
      }|] >>= ok

setPara :: PrimMonad m => Bidi (PrimState m) -> Text -> Level -> Maybe (Prim.Vector Level) -> m ()
setPara bidi text paraLevel els = unsafeIOToPrim do
  useAsPtr text \t i16@(fromIntegral -> len) -> do
    (fromMaybe nullPtr -> u) <- for els \(Prim.Vector vofs vlen (ByteArray vba)) -> do
      u <- if vlen < len
           then callocBytes (fromIntegral i16)
           else mallocBytes (fromIntegral i16)
      u <$ copyPrimArrayToPtr u (PrimArray vba) vofs vlen -- missing from Data.Vector
    let n = fromIntegral i16
    [C.block|UErrorCode {
      UErrorCode error_code =0;
      ubidi_setPara(
        $bidi:bidi,
        $(const UChar * t),
        $(int32_t n),
        $(UBiDiLevel paraLevel),
        $(UBiDiLevel * u),
        &error_code
      );
      return error_code;
    }|] >>= ok
    v <- atomicModifyIORef (embeddingLevels bidi) (u,)
    when (v /= nullPtr) do free v

setLine :: PrimMonad m => Bidi (PrimState m) -> Int32 -> Int32 -> Bidi (PrimState m) -> m ()
setLine para start limit line = unsafeIOToPrim do
  [C.block|UErrorCode {
    UErrorCode error_code = 0;
    ubidi_setLine(
      $bidi:para,
      $(int32_t start),
      $(int32_t limit),
      $bidi:line,
      &error_code
    );
    return error_code;
  }|] >>= ok
  writeIORef (parentLink line) $ Just para -- prevents deallocation of the paragraph bidi before we at least repurpose the line

getDirection :: PrimMonad m => Bidi (PrimState m) -> m Direction
getDirection bidi = unsafeIOToPrim do
  [C.exp|UBiDiDirection { ubidi_getDirection($bidi:bidi) }|] <&> toEnum . fromIntegral

getBaseDirection :: Text -> Direction
getBaseDirection text = unsafeLocalState do
  useAsPtr text \t (fromIntegral -> len) ->
    [C.exp|UBiDiDirection { ubidi_getBaseDirection($(const UChar * t),$(int32_t len)) }|] <&> toEnum . fromIntegral

getParaLevel :: PrimMonad m => Bidi (PrimState m) -> m Level
getParaLevel bidi = unsafeIOToPrim [C.exp|UBiDiLevel { ubidi_getParaLevel($bidi:bidi) }|]

countParagraphs :: PrimMonad m => Bidi (PrimState m) -> m Int32
countParagraphs bidi = unsafeIOToPrim [C.exp|int32_t { ubidi_countParagraphs($bidi:bidi) }|]

-- | Given a paragraph or line bidirectional object @bidi@, and a @charIndex@ into the text
-- in the range @0@ to @'getProcessedLength' bidi -1@, this will return
-- the index of the paragraph, the index of the first character in the text,
-- the index of the end of the paragraph, and the level of the paragraph.
--
-- If the paragraph index is known, it can be more efficient to use 'getParagraphByIndex'
getParagraph :: PrimMonad m => Bidi (PrimState m) -> Int32 -> m (Int32, Int32, Int32, Level)
getParagraph bidi charIndex = unsafeIOToPrim do
  allocaArray 2 \pParaStart ->
    alloca \pParaLevel ->
      with def \pErrorCode -> do
        result <- [C.block|int32_t {
          int32_t * pPara = $(int32_t * pParaStart);
          return ubidi_getParagraph(
            $bidi:bidi,
            $(int32_t charIndex),
            pPara,
            pPara+1,
            $(UBiDiLevel * pParaLevel),
            $(UErrorCode * pErrorCode)
          );
        }|]
        peek pErrorCode >>= ok
        (,,,) result
          <$> peek pParaStart
          <*> peek (Prim.advancePtr pParaStart 1) -- pParaLimit
          <*> peek pParaLevel

getParagraphByIndex :: PrimMonad m => Bidi (PrimState m) -> Int32 -> m (Int32, Int32, Level)
getParagraphByIndex bidi paragraphIndex = unsafeIOToPrim do
  allocaArray 2 \pParaStart ->
    alloca \pParaLevel -> do
        [C.block|UErrorCode {
          int32_t * pPara = $(int32_t * pParaStart);
          UErrorCode error_code = 0;
          ubidi_getParagraph(
            $bidi:bidi,
            $(int32_t paragraphIndex),
            pPara,
            pPara+1,
            $(UBiDiLevel * pParaLevel),
            &error_code
          );
          return error_code;
        }|] >>= ok
        (,,) <$> peek pParaStart
             <*> peek (Prim.advancePtr pParaStart 1) -- pParaLimit
             <*> peek pParaLevel


getLevelAt :: PrimMonad m => Bidi (PrimState m) -> Int32 -> m Level
getLevelAt bidi charIndex = unsafeIOToPrim [C.exp|UBiDiLevel { ubidi_getLevelAt($bidi:bidi,$(int32_t charIndex)) }|]

getLogicalRun :: PrimMonad m => Bidi (PrimState m) -> Int32 -> m (Int32, Level)
getLogicalRun bidi logicalPosition = unsafeIOToPrim do
  alloca \pLevel ->
    (,) <$> [C.block|int32_t {
              int32_t logicalLimit;
              ubidi_getLogicalRun(
                $bidi:bidi,
                $(int32_t logicalPosition),
                &logicalLimit,
                $(UBiDiLevel * pLevel)
              );
              return logicalLimit;
            }|]
        <*> peek pLevel

countRuns :: PrimMonad m => Bidi (PrimState m) -> m Int32
countRuns bidi = unsafeIOToPrim do
  with def \pErrorCode -> do
    [C.exp|int32_t {
      ubidi_countRuns($bidi:bidi, $(UErrorCode * pErrorCode))
    }|] <* (peek pErrorCode >>= ok)

-- | Get one run's logical start, length, and directionality which will be LTR or RTL.
--
-- 'countRuns' should be called before the runs are retrieved
getVisualRun :: PrimMonad m => Bidi (PrimState m) -> Int32 -> m (Int32, Int32, Direction)
getVisualRun bidi runIndex = unsafeIOToPrim do
  allocaArray 2 \pLogicalStart -> do
    dir <- [C.block|UBiDiDirection {
      int32_t * pLogicalStart = $(int32_t * pLogicalStart);
      return ubidi_getVisualRun(
        $bidi:bidi,
        $(int32_t runIndex),
        pLogicalStart,
        pLogicalStart+1 /* pLength */
      );
    }|] <&> toEnum . fromIntegral
    logical_start <- peek pLogicalStart
    len <- peek (Prim.advancePtr pLogicalStart 1) -- pLength
    pure (logical_start, len, dir)

invertMap :: PrimArray Int32 -> PrimArray Int32
invertMap pa = unsafePerformIO do -- use a full heavy weight dup check as this can be slow for large maps
  let !n = sizeofPrimArray pa
  let !m = fromIntegral (foldlPrimArray' max (-1) pa + 1)
  allocaArray (n+m) \srcMap -> do
    copyPrimArrayToPtr srcMap pa 0 n
    let len = fromIntegral n
    [C.block|void {
      int32_t * srcMap = $(int32_t * srcMap);
      int32_t len = $(int32_t len);
      ubidi_invertMap(srcMap,srcMap+len,len);
    }|]
    peekPrimArray m (Prim.advancePtr srcMap n) -- dstMap

getVisualIndex :: PrimMonad m => Bidi (PrimState m) -> Int32 -> m Int32
getVisualIndex bidi logicalIndex = unsafeIOToPrim do
  with def \pErrorCode ->
    [C.exp|int32_t {
      ubidi_getVisualIndex($bidi:bidi,$(int32_t logicalIndex),$(UErrorCode * pErrorCode))
    }|] <* (peek pErrorCode >>= ok)

getLogicalIndex :: PrimMonad m => Bidi (PrimState m) -> Int32 -> m Int32
getLogicalIndex bidi visualIndex = unsafeIOToPrim do
  with def \pErrorCode ->
    [C.exp|int32_t {
      ubidi_getLogicalIndex($bidi:bidi,$(int32_t visualIndex),$(UErrorCode * pErrorCode))
    }|] <* (peek pErrorCode >>= ok)

getLogicalMap :: PrimMonad m => Bidi (PrimState m) -> m (PrimArray Int32)
getLogicalMap bidi = stToPrim do
  len <- fromIntegral <$> do
    opts <- getReorderingOptions bidi
    processed_len <- getProcessedLength bidi
    if opts .&. OPTION_INSERT_MARKS /= OPTION_DEFAULT
    then max processed_len <$> getResultLength bidi
    else pure processed_len
  unsafeIOToPrim do
    allocaArray len \ indexMap -> do
      [C.block|UErrorCode {
        UErrorCode error_code = 0;
        ubidi_getLogicalMap($bidi:bidi,$(int32_t * indexMap),&error_code);
        return error_code;
      }|] >>= ok
      peekPrimArray len indexMap

getVisualMap :: PrimMonad m => Bidi (PrimState m) -> m (PrimArray Int32)
getVisualMap bidi = stToPrim do
  len <- fromIntegral <$> do
    opts <- getReorderingOptions bidi
    result_len <- getResultLength bidi
    if opts .&. OPTION_INSERT_MARKS /= OPTION_DEFAULT
    then max result_len <$> getProcessedLength bidi
    else pure result_len
  unsafeIOToPrim do
    allocaArray len \indexMap -> do
      [C.block|UErrorCode {
        UErrorCode error_code = 0;
        ubidi_getVisualMap($bidi:bidi,$(int32_t * indexMap),&error_code);
        return error_code;
      }|] >>= ok
      peekPrimArray len indexMap

getResultLength :: PrimMonad m => Bidi (PrimState m) -> m Int32
getResultLength bidi = unsafeIOToPrim [C.exp|int32_t { ubidi_getProcessedLength($bidi:bidi) }|]

getProcessedLength :: PrimMonad m => Bidi (PrimState m) -> m Int32
getProcessedLength bidi = unsafeIOToPrim [C.exp|int32_t { ubidi_getProcessedLength($bidi:bidi) }|]

getLevels :: PrimMonad m => Bidi (PrimState m) -> m (PrimArray Level)
getLevels bidi = stToPrim do
  len <- fromIntegral <$> getProcessedLength bidi
  unsafeIOToPrim $
    with def \pErrorCode -> do
       levels <- [C.exp|const UBiDiLevel * { ubidi_getLevels($bidi:bidi, $(UErrorCode * pErrorCode)) }|]
       peek pErrorCode >>= ok
       peekPrimArray len levels

reorderLogical :: PrimArray Level -> PrimArray Int32
reorderLogical pa = unsafePerformIO do
  withPrimArrayLen pa \n@(fromIntegral -> len) levels ->
    allocaArray n \indexMap ->
      [C.block|void {
        ubidi_reorderLogical($(const UBiDiLevel * levels),$(int32_t len),$(int32_t * indexMap));
      }|] *> peekPrimArray n indexMap

reorderVisual :: PrimArray Level -> PrimArray Int32
reorderVisual pa = unsafePerformIO do
  withPrimArrayLen pa \n@(fromIntegral -> len) levels ->
    allocaArray n \indexMap ->
      [C.block|void {
        ubidi_reorderVisual($(const UBiDiLevel * levels),$(int32_t len),$(int32_t * indexMap));
      }|] *> peekPrimArray n indexMap

getCustomizedClass :: PrimMonad m => Bidi (PrimState m) -> Char -> m CharDirection
getCustomizedClass bidi (fromIntegral . fromEnum -> c) = unsafeIOToPrim
  [C.exp|UCharDirection { ubidi_getCustomizedClass($bidi:bidi, $(UChar32 c)) }|]

setClassCallback :: PrimMonad m => Bidi (PrimState m) -> FunPtr ClassCallback -> Ptr () -> m (FunPtr ClassCallback, Ptr ())
setClassCallback bidi newFn newCtx = unsafeIOToPrim do
  alloca \oldFn ->
    alloca \oldCtx -> do
      [C.block|UErrorCode {
        UErrorCode error_code = 0;
        ubidi_setClassCallback(
          $bidi:bidi,
          $(UBiDiClassCallbackPtr newFn),
          $(const void * newCtx),
          $(UBiDiClassCallbackPtr * oldFn),
          $(const void ** oldCtx),
          &error_code
        );
        return error_code;
      }|] >>= ok
      (,) <$> peek oldFn <*> peek oldCtx

getClassCallback :: PrimMonad m => Bidi (PrimState m) -> m (FunPtr ClassCallback, Ptr ())
getClassCallback bidi = unsafeIOToPrim do
  alloca \fn ->
    alloca \ctx ->
      (,) <$  [C.block|void { ubidi_getClassCallback($bidi:bidi,$(UBiDiClassCallbackPtr * fn),$(const void ** ctx)); }|]
          <*> peek fn
          <*> peek ctx

writeReordered :: PrimMonad m => Bidi (PrimState m) -> WriteOptions -> m Text
writeReordered bidi options = stToPrim do
  destSize@(fromIntegral -> len) <- if options .&. INSERT_LRM_FOR_NUMERIC /= def
    then do
      len <- getLength bidi
      runs <- countRuns bidi
      pure $ len + 2 * runs
    else getProcessedLength bidi
  unsafeIOToPrim do
    allocaArray len \dest ->
      with def \pErrorCode -> do
        actual_len <- [C.exp|int32_t {
          ubidi_writeReordered($bidi:bidi,$(UChar * dest),$(int32_t destSize),$(WriteOptions options),$(UErrorCode * pErrorCode))
        }|]
        peek pErrorCode >>= ok
        fromPtr dest $ fromIntegral actual_len

writeReverse :: Text -> WriteOptions -> Text
writeReverse t options = unsafePerformIO do
  useAsPtr t \src i16@(fromIntegral -> n) ->
    allocaArray (fromIntegral i16) \ dest ->
      with def \pErrorCode -> do
        actual_len <- [C.block|int32_t {
          int32_t len = $(int32_t n);
          return ubidi_writeReverse($(const UChar * src),len,$(UChar * dest),len,$(WriteOptions options),$(UErrorCode * pErrorCode));
        }|]
        peek pErrorCode >>= ok
        fromPtr dest $ fromIntegral actual_len