{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE StandaloneDeriving #-}

module Hercules.API.Attribute
  ( AttributeType (..),
    Attribute (..),
    AttributePath (..),
    attributePathFromString,
    attributePathToString,
  )
where

import Control.Applicative (Alternative ((<|>)))
import Control.Lens (at, (%~))
import Data.Aeson qualified as A
import Data.Aeson.Lens
import Data.Function ((&))
import Data.OpenApi qualified as O3
import Data.Proxy (Proxy (Proxy))
import Data.Swagger (ToParamSchema (..))
import Data.Text qualified as T
import Hercules.API.Prelude
import Servant.API (FromHttpApiData (..), ToHttpApiData (..))
import Prelude ()

data AttributeType
  = Regular
  | MustFail
  | MayFail
  | DependenciesOnly
  | Effect
  deriving (Generic, Show, Eq)
  deriving anyclass (NFData, ToJSON, FromJSON, ToSchema, O3.ToSchema)

-- | An arbitrary ordering
deriving instance Ord AttributeType

data Attribute a = Attribute
  { path :: [Text],
    value :: a,
    typ :: AttributeType
  }
  deriving (Generic, Show, Eq)
  deriving anyclass (NFData, ToJSON)

instance (FromJSON a) => FromJSON (Attribute a) where
  parseJSON v = A.parseJSON (fixup v)
    where
      fixup :: A.Value -> A.Value
      fixup = _Object . at "typ" %~ (<|> Just (A.String "Regular"))

deriving instance (ToSchema a) => ToSchema (Attribute a)

deriving instance (O3.ToSchema a) => O3.ToSchema (Attribute a)

deriving instance Functor Attribute

deriving instance Foldable Attribute

deriving instance Traversable Attribute

newtype AttributePath = AttributePath {fromAttributePath :: [Text]}
  deriving (Generic, Eq)
  deriving anyclass (NFData)

instance FromHttpApiData AttributePath where
  parseUrlPiece = Right . AttributePath . attributePathFromString

  parseQueryParam = parseUrlPiece

instance ToParamSchema AttributePath where
  toParamSchema _ = toParamSchema (Proxy :: Proxy Text)

instance ToHttpApiData AttributePath where
  toUrlPiece = toUrlPiece . attributePathToString . fromAttributePath

instance O3.ToParamSchema AttributePath where
  toParamSchema _ = O3.toParamSchema (Proxy :: Proxy Text)

----------------------------------------

attributePathToString :: [Text] -> Text
attributePathToString l =
  l & map stringToNixIdentifier & T.intercalate "."

stringToNixIdentifier :: Text -> Text
stringToNixIdentifier s =
  if isNixSimpleId s
    then s
    else nixQuote s

isNixSimpleId :: Text -> Bool
isNixSimpleId t | T.null t = False
isNixSimpleId t
  | not
      ( let h = T.head t
         in (h >= 'a' && h <= 'z')
              || (h >= 'A' && h <= 'Z')
              || h
              == '_'
      ) =
      False
isNixSimpleId t =
  T.all
    ( \c ->
        (c >= 'a' && c <= 'z')
          || (c >= 'A' && c <= 'Z')
          || (c >= '0' && c <= '9')
          || c
          == '_'
          || c
          == '\''
          || c
          == '-'
    )
    t

nixQuote :: Text -> Text
nixQuote s =
  "\""
    <> T.replace "\"" "\\\"" (T.replace "\\" "\\\\" s)
    <> "\""

----------------------------------------

attributePathFromString :: Text -> [Text]
attributePathFromString "" = []
attributePathFromString t = t & T.unpack & attributePathFromStringImpl [] "" & reverse & fmap T.pack

attributePathFromStringImpl :: [String] -> String -> [Char] -> [String]
attributePathFromStringImpl = \accPath accAttr chars -> case chars of
  [] -> reverse accAttr : accPath
  ('.' : cs) -> attributePathFromStringImpl (reverse accAttr : accPath) "" cs
  ('\"' : cs) -> attributePathFromStringElement accPath "" cs
  (c : cs) -> attributePathFromStringImpl accPath (c : accAttr) cs

attributePathFromStringElement :: [String] -> String -> [Char] -> [String]
attributePathFromStringElement = \accPath accAttr chars -> case chars of
  ('"' : cs) -> attributePathFromStringImpl accPath accAttr cs
  ('\\' : c : cs) -> attributePathFromStringElement (accPath) (c : accAttr) cs
  [] -> reverse accAttr : accPath
  (c : cs) -> attributePathFromStringElement accPath (c : accAttr) cs
