{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE TemplateHaskell #-}

module Language.Nix.Identifier
  ( Identifier, ident, quote, needsQuoting
  , parseSimpleIdentifier, parseQuotedIdentifier
  )
  where

import Control.DeepSeq
import Control.Lens
import Data.Char
import Data.String
import Distribution.Compat.ReadP as ReadP
import Distribution.Text
import GHC.Generics ( Generic )
import Prelude.Compat
import Test.QuickCheck
import Text.PrettyPrint as PP

-- | Identifiers in Nix are essentially strings. They can be constructed
-- (and viewed) with the 'ident' isomorphism. For the sake of convenience,
-- @Identifier@s are an instance of the 'IsString' class.
--
-- Reasonable people restrict themselves to identifiers of the form
-- @[a-zA-Z_][a-zA-Z0-9_'-]*@, because these don't need quoting. The
-- methods of the 'Text' class can be used to parse and pretty-print an
-- identifier with proper quoting:
--
-- >>> disp (ident # "test")
-- test
-- >>> disp (ident # "foo.bar")
-- "foo.bar"
--
-- prop> \str -> Just (ident # str) == simpleParse (quote str)
-- prop> \i -> Just (i :: Identifier) == simpleParse (display i)
declareLenses [d| newtype Identifier = Identifier { ident :: String }
                    deriving (Show, Eq, Ord, IsString, Generic)
              |]
-- ^ An isomorphism that allows conversion of 'Identifier' from/to the
-- standard 'String' type via 'review'.
--
-- prop> \str -> fromString str == ident # str
-- prop> \str -> set ident str undefined == ident # str
-- prop> \str -> view ident (review ident str) == str

instance NFData Identifier where
  rnf (Identifier str) = rnf str

instance Arbitrary Identifier where
  arbitrary = Identifier <$> arbitrary
  shrink (Identifier i) = map Identifier (shrink i)

instance Text Identifier where
  disp = view (ident . to quote . to text)
  parse = parseQuotedIdentifier <++ parseSimpleIdentifier

-- | 'ReadP' parser for simple identifiers, i.e. those that don't need
-- quoting.
parseSimpleIdentifier :: ReadP r Identifier
parseSimpleIdentifier = do
  c <- satisfy (\x -> x == '_' || isAlpha x)
  cs <- munch (\x -> x `elem` "_'-" || isAlphaNum x)
  return (Identifier (c:cs))

-- | 'ReadP' parser for quoted identifiers, i.e. those that /do/ need
-- quoting.
parseQuotedIdentifier :: ReadP r Identifier
parseQuotedIdentifier = Identifier . read . fst
                     <$> gather (between (ReadP.char '"') (ReadP.char '"') (many qString))
  where
    qString :: ReadP r String
    qString = quotedPair <++ munch1 (`notElem` "\\\"")

    quotedPair :: ReadP r String
    quotedPair = do
      c1 <- ReadP.char '\\'
      c2 <- get
      return [c1,c2]

-- | Checks whether a given string needs quoting when interpreted as an
-- 'Identifier'. Simple identifiers that don't need quoting match the
-- regular expression @^[a-zA-Z_][a-zA-Z0-9_'-]*$@.
needsQuoting :: String -> Bool
needsQuoting s = null r || not (any (null . snd) r)
  where r = readP_to_S parseSimpleIdentifier s

-- | Helper function to quote a given identifier string if necessary.
--
-- >>> putStrLn (quote "abc")
-- abc
-- >>> putStrLn (quote "abc.def")
-- "abc.def"
quote :: String -> String
quote s = if needsQuoting s then show s else s