{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE FlexibleInstances #-}
-- | Valid Haskell qualified module names.
module FP.API.ModuleName
  ( ModuleName(..)
  , toModulePath
  , mainModule
  , checkModuleName
  , fromModuleName
  , isModulePath
  , fromModulePath
  ) where

import           Prelude
import           Control.Monad (mplus, guard)
import qualified Data.Char as Char (isAlphaNum, isUpper)
import           Data.Maybe (isJust)
import           Data.Text (Text)
import           Data.Text (splitOn)
import qualified Data.Text as T
import           Data.Monoid ((<>))
import           FP.API.Types

toModulePath :: ModuleName -> Text
toModulePath (ModuleName n) = T.intercalate "/" (splitOn "." n) <> ".hs"

mainModule :: ModuleName
mainModule = ModuleName "Main"

checkModuleName :: ModuleName -> Either Text ModuleName
checkModuleName (ModuleName mn) = maybe (Left ("Invalid module name: " <> mn)) Right $ fromModuleName mn

-- | Parses normal modules, or main modules with an additional filepath.
fromModuleName :: Text -> Maybe ModuleName
fromModuleName string
    | all validModuleComponent components && not (null components) = Just $ ModuleName string
    | otherwise                                                    = Nothing
  where
    components = splitOn "." string

-- | Determine if the given textual @FilePath@ might be a valid module path.
isModulePath :: Text -> Bool
isModulePath t = isJust $ T.stripSuffix ".hs" t `mplus` T.stripSuffix ".lhs" t

-- | Guesses a module name from a path, assuming that it ends in ".hs", and
--   every path component is a component of the name.  Should be the inverse
--   of 'toModulePath' (other than the 'Just').
fromModulePath :: Text -> Maybe ModuleName
fromModulePath s1 = do
    s2 <- T.stripSuffix ".hs" s1
    let components = splitOn "/" s2
    guard $ all validModuleComponent components
    guard $ not (null components)
    return $ ModuleName $ T.intercalate "." components

validModuleChar :: Char -> Bool
validModuleChar c = Char.isAlphaNum c || c == '_' || c == '\''

validModuleComponent :: Text -> Bool
validModuleComponent t =
    case T.uncons t of
        Nothing -> False
        Just (c, t') -> Char.isUpper c && T.all validModuleChar t'