{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications  #-}

module Bricks.UnquotedString
  (
  -- * Type
    UnquotedString

  -- * Construction
  , unquotedString'try
  , unquotedString'orThrow

  -- * Deconstruction
  , unquotedString'text

  -- * Predicates
  , text'canBeUnquoted
  , char'canBeUnquoted

  ) where

-- Bricks
import Bricks.Keyword

-- Bricks internal
import           Bricks.Internal.Prelude
import           Bricks.Internal.Text    (Text)
import qualified Bricks.Internal.Text    as Text

-- Base
import qualified Data.Char as Char
import qualified Data.List as List
import           Prelude   (error)

{- | A string that can be rendered unquoted. Unquoted strings are restricted to
a conservative set of characters; see 'text'canBeUnquoted' for the full rules.

This type does not represent a particular part of Bricks syntax, but it is a
wrapper for 'Text' that enforces the limitations of strings at various places in
the Bricks syntax.

==== Construction

  - 'unquotedString'try'
  - 'unquotedString'orThrow'

==== Deconstruction

  - 'unquotedString'text'

==== See also

  - 'text'canBeUnquoted'
  - 'char'canBeUnquoted'

-}

newtype UnquotedString = UnquotedString { unquotedString'text :: Text }

instance Show UnquotedString where show = show @Text . unquotedString'text

{- | ==== Properties

  - A text value may be used to construct an 'UnquotedString' iff it satisfies
    'text'canBeUnquoted'.

      @'text'canBeUnquoted' x = 'isJust' ('unquotedString'try' x)@ -}

unquotedString'try :: Text -> Maybe UnquotedString
unquotedString'try x =
  if text'canBeUnquoted x then Just (UnquotedString x) else Nothing

-- | Throws an exception if the string cannot render unquoted.
unquotedString'orThrow :: Text -> UnquotedString
unquotedString'orThrow x =
  if text'canBeUnquoted x then UnquotedString x else
  error $ "String " <> show x <> " cannot render unquoted"

{- | Whether a string having this name can be rendered without quoting it.

==== Requirements for unquoted strings

We allow a string to render unquoted if all these conditions are met:

- The string is nonempty
- All characters satify 'char'canBeUnquoted'
- The string is not a keyword

==== Properties

  - A text value may be used to construct an 'UnquotedString' iff it satisfies
    'text'canBeUnquoted'.

      @'text'canBeUnquoted' x = 'isJust' ('unquotedString'try' x)@ -}

-- | ==== Examples
--
-- >>> text'canBeUnquoted "-ab_c"
-- True
--
-- >>> text'canBeUnquoted ""
-- False
--
-- >>> text'canBeUnquoted "a\"b"
-- False
--
-- >>> text'canBeUnquoted "let"
-- False

text'canBeUnquoted :: Text -> Bool
text'canBeUnquoted x =
  Text.all char'canBeUnquoted x
  && not (Text.null x)
  && List.all ((/= x) . keywordText) keywords

{- | Whether the character is allowed to be included in an 'UnquotedString'.
Such characters are letters, @+@, @-@, @*@, @/@, and @_@.

This is used in the implementation of 'text'canBeUnquoted'. -}

char'canBeUnquoted :: Char -> Bool
char'canBeUnquoted c =
  Char.isLetter c || List.elem c ("+-*/_" :: [Char])