content: {-# LANGUAGE CPP #-}
  
  module Data.Aeson.TypeScript.Formatting where
  
  import Data.Aeson as A
  import Data.Aeson.TypeScript.Types
  import qualified Data.ByteString.Lazy.Char8 as BL8
  import Data.Function ((&))
  import qualified Data.List as L
  import Data.Maybe
  import Data.String.Interpolate
  import qualified Data.Text as T
  
  #if !MIN_VERSION_base(4,11,0)
  import Data.Monoid
  #endif
  
  
  -- | Same as 'formatTSDeclarations'', but uses default formatting options.
  formatTSDeclarations :: [TSDeclaration] -> String
  formatTSDeclarations = formatTSDeclarations' defaultFormattingOptions
  
  -- | Format a single TypeScript declaration. This version accepts a FormattingOptions object in case you want more control over the output.
  formatTSDeclaration :: FormattingOptions -> TSDeclaration -> String
  formatTSDeclaration (FormattingOptions {..}) (TSTypeAlternatives name genericVariables names maybeDoc) =
    makeDocPrefix maybeDoc <> mainDeclaration
    where
      mainDeclaration = case chooseTypeAlternativesFormat typeAlternativesFormat of
        Enum -> [i|#{exportPrefix exportMode}enum #{typeNameModifier name} { #{alternativesEnum} }|]
          where
            alternativesEnum = T.intercalate ", " $ [toEnumName entry <> "=" <> entry | entry <- T.pack <$> names]
        EnumWithType -> [i|#{exportPrefix exportMode}enum #{typeNameModifier name}Enum { #{alternativesEnumWithType} }#{enumType}|]
          where
            alternativesEnumWithType = T.intercalate ", " $ [toEnumName entry <> "=" <> entry | entry <- T.pack <$> names]
            enumType = [i|\n\ntype #{name} = keyof typeof #{typeNameModifier name}Enum;|] :: T.Text
        TypeAlias -> [i|#{exportPrefix exportMode}type #{typeNameModifier name}#{getGenericBrackets genericVariables} = #{alternatives};|]
          where
            alternatives = T.intercalate " | " (fmap T.pack names)
  
      -- Only allow certain formats if some checks pass
      chooseTypeAlternativesFormat Enum
        | all isDoubleQuotedString names = Enum
        | otherwise = TypeAlias
      chooseTypeAlternativesFormat EnumWithType
        | all isDoubleQuotedString names = EnumWithType
        | otherwise = TypeAlias
      chooseTypeAlternativesFormat x = x
  
      isDoubleQuotedString s = case A.eitherDecode (BL8.pack s) of
        Right (A.String _) -> True
        _ -> False
  
      toEnumName = T.replace "\"" ""
  
  formatTSDeclaration (FormattingOptions {..}) (TSInterfaceDeclaration interfaceName genericVariables (filter (not . isNoEmitTypeScriptField) -> members) maybeDoc) =
    makeDocPrefix maybeDoc <> [i|#{exportPrefix exportMode}interface #{modifiedInterfaceName}#{getGenericBrackets genericVariables} {
  #{ls}
  }|] where
        ls = T.intercalate "\n" $ [indentTo numIndentSpaces (T.pack (formatTSField member <> ";")) | member <- members]
        modifiedInterfaceName = (\(li, name) -> li <> interfaceNameModifier name) . splitAt 1 $ interfaceName
  
        formatTSField :: TSField -> String
        formatTSField (TSField optional name typ maybeDoc') = makeDocPrefix maybeDoc' <> [i|#{name}#{if optional then ("?" :: String) else ""}: #{typ}|]
  
  formatTSDeclaration _ (TSRawDeclaration text) = text
  
  indentTo :: Int -> T.Text -> T.Text
  indentTo numIndentSpaces input = T.intercalate "\n" [padding <> line | line <- T.splitOn "\n" input]
    where padding = T.replicate numIndentSpaces " "
  
  exportPrefix :: ExportMode -> String
  exportPrefix ExportEach = "export "
  exportPrefix ExportNone = ""
  
  -- | Format a list of TypeScript declarations into a string, suitable for putting directly into a @.d.ts@ file.
  formatTSDeclarations' :: FormattingOptions -> [TSDeclaration] -> String
  formatTSDeclarations' options allDeclarations =
    declarations & fmap (T.pack . formatTSDeclaration options)
                 & T.intercalate "\n\n"
                 & T.unpack
    where
      removedDeclarationNames = mapMaybe getDeclarationName (filter isNoEmitTypeScriptDeclaration allDeclarations)
        where
          getDeclarationName :: TSDeclaration -> Maybe String
          getDeclarationName (TSInterfaceDeclaration {..}) = Just interfaceName
          getDeclarationName (TSTypeAlternatives {..}) = Just typeName
          getDeclarationName _ = Nothing
  
      removeReferencesToRemovedNames :: [String] -> TSDeclaration -> TSDeclaration
      removeReferencesToRemovedNames removedNames decl@(TSTypeAlternatives {..}) = decl { alternativeTypes = [x | x <- alternativeTypes, not (x `L.elem` removedNames)] }
      removeReferencesToRemovedNames _ x = x
  
      declarations = allDeclarations
                   & filter (not . isNoEmitTypeScriptDeclaration)
                   & fmap (removeReferencesToRemovedNames removedDeclarationNames)
  
  makeDocPrefix :: Maybe String -> String
  makeDocPrefix maybeDoc = case maybeDoc of
    Nothing -> ""
    Just (T.pack -> text) -> ["// " <> line | line <- T.splitOn "\n" text]
                          & T.intercalate "\n"
                          & (<> "\n")
                          & T.unpack
  
  getGenericBrackets :: [String] -> String
  getGenericBrackets [] = ""
  getGenericBrackets xs = [i|<#{T.intercalate ", " (fmap T.pack xs)}>|]
  
  -- * Support for @no-emit-typescript
  
  noEmitTypeScriptAnnotation :: String
  noEmitTypeScriptAnnotation = "@no-emit-typescript"
  
  isNoEmitTypeScriptField :: TSField -> Bool
  isNoEmitTypeScriptField (TSField {fieldDoc=(Just doc)}) = noEmitTypeScriptAnnotation `L.isInfixOf` doc
  isNoEmitTypeScriptField _ = False
  
  isNoEmitTypeScriptDeclaration :: TSDeclaration -> Bool
  isNoEmitTypeScriptDeclaration (TSInterfaceDeclaration {interfaceDoc=(Just doc)}) = noEmitTypeScriptAnnotation `L.isInfixOf` doc
  isNoEmitTypeScriptDeclaration (TSTypeAlternatives {typeDoc=(Just doc)}) = noEmitTypeScriptAnnotation `L.isInfixOf` doc
  isNoEmitTypeScriptDeclaration _ = False
  