{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}

-- | Functionality to split models into multiple modules according to their dependencies
module OpenAPI.Generate.ModelDependencies
  ( getModelModulesFromModelsWithDependencies,
    ModuleDefinition,
    Models,
    ModelContentWithDependencies,
    ModelWithDependencies,
  )
where

import Data.List
import qualified Data.Set as Set
import qualified Data.Text as T
import Data.Text (Text)
import Language.Haskell.TH
import Language.Haskell.TH.PprLib hiding ((<>))
import qualified OpenAPI.Generate.Doc as Doc
import OpenAPI.Generate.Internal.Util

-- | A module definition with a name as a string list with the different module levels (e. g. [\"OpenAPI\", \"Generate\"] for "OpenAPI.Generate")
-- and the 'Doc' representing the module content
type ModuleDefinition = ([String], Doc)

-- | A set of model names (typically used as dependency list)
type Models = Set.Set Text

-- | A tuple containing the content and the dependencies of a model
type ModelContentWithDependencies = (Q Doc, Models)

-- | Represents a model with a name, content and dependencies
type ModelWithDependencies = (Text, ModelContentWithDependencies)

typesModule :: String
typesModule = "Types"

cyclicTypesModule :: String
cyclicTypesModule = "CyclicTypes"

-- | Analyzes the dependencies of the provided models and splits them into modules.
-- All models with cyclic dependencies (between each other or to itself) are put in a module named by @cyclicTypesModule@.
getModelModulesFromModelsWithDependencies :: String -> [ModelWithDependencies] -> Q [ModuleDefinition]
getModelModulesFromModelsWithDependencies mainModuleName = createModelModules mainModuleName . extractCyclicModuleDependentModels

createModelModules :: String -> ([ModelWithDependencies], Q Doc) -> Q [ModuleDefinition]
createModelModules mainModuleName (models, cyclicModuleContentQ) = do
  let prependTypesModule = ((typesModule <> ".") <>) . T.unpack
  let prependMainModule = ((mainModuleName <> ".") <>)
  cyclicModuleContent <- cyclicModuleContentQ
  modules <-
    mapM
      ( \(modelName, (doc, dependencies)) ->
          ([typesModule, T.unpack modelName],)
            . Doc.addModelModuleHeader
              mainModuleName
              (prependTypesModule modelName)
              (prependTypesModule <$> Set.toList dependencies)
              ("Contains the types generated from the schema " <> T.unpack modelName)
              <$> doc
      )
      models
  let modelModuleNames = fmap (joinWithPoint . fst) modules
  pure $
    ( [typesModule],
      Doc.createModuleHeaderWithReexports
        (prependMainModule typesModule)
        (fmap prependMainModule (cyclicTypesModule : modelModuleNames))
        "Rexports all type modules (used in the operation modules)."
    )
      : ( [cyclicTypesModule],
          Doc.addModelModuleHeader
            mainModuleName
            cyclicTypesModule
            modelModuleNames
            "Contains all types with cyclic dependencies (between each other or to itself)"
            cyclicModuleContent
        )
      : modules

extractCyclicModuleDependentModels :: [ModelWithDependencies] -> ([ModelWithDependencies], Q Doc)
extractCyclicModuleDependentModels models =
  let (cyclicModels, extractedModels) = extractUnidirectionallyDependentModels (models, [])
   in (extractedModels, vcat <$> mapM (fst . snd) cyclicModels)

extractUnidirectionallyDependentModels :: ([ModelWithDependencies], [ModelWithDependencies]) -> ([ModelWithDependencies], [ModelWithDependencies])
extractUnidirectionallyDependentModels (rest, extractedModels) =
  let extractedModelNames = Set.fromList $ fmap fst extractedModels
      (newExtractedModels, notExtractedModels) = partition ((`Set.isSubsetOf` extractedModelNames) . snd . snd) rest
   in if null newExtractedModels
        then (rest, extractedModels)
        else extractUnidirectionallyDependentModels (notExtractedModels, extractedModels <> newExtractedModels)
