module Require where

import qualified Data.Text as Text
import Options.Generic
import Relude
import System.Directory
import qualified Text.Megaparsec as Megaparsec
import qualified Text.Megaparsec.Char as Megaparsec

newtype FileName = FileName {unFileName :: Text}

newtype LineNumber = LineNumber Int

type Parser = Megaparsec.Parsec Void Text

data RequireInfo
  = RequireInfo
      { riFullModuleName :: Text,
        riModuleAlias :: Text,
        riImportedTypes :: Maybe [Text]
      }
  deriving (Show)

data CommandArguments
  = CommandArguments Text Text Text
  deriving (Generic)

instance ParseRecord CommandArguments

findRequires :: IO (Maybe Text)
findRequires = do
  currentDir <- getCurrentDirectory
  files <- getDirectoryContents currentDir
  let textFiles = fmap toText files
  return $ head <$> nonEmpty (filter (Text.isSuffixOf "Requires") textFiles)

requireMain :: IO ()
requireMain = do
  CommandArguments inputFile _ outputFile <- getRecord "Require Haskell preprocessor" :: IO CommandArguments
  content <- readFile (toString inputFile)
  writeFile
    (toString outputFile)
    ( toString $
        Require.transform
          False
          (Require.FileName inputFile)
          ""
          (toText content)
    )

autorequireMain :: IO ()
autorequireMain = do
  CommandArguments inputFile _ outputFile <- getRecord "Require Haskell preprocessor" :: IO CommandArguments
  requiresFile <- findRequires
  case requiresFile of
    Nothing -> die "There is no Requires file in the system"
    Just x -> do
      file <- readFile $ toString x
      content <- readFile (toString inputFile)
      writeFile
        (toString outputFile)
        ( toString $
            Require.transform
              True
              (Require.FileName inputFile)
              (toText file)
              (toText content)
        )

transform :: Bool -> FileName -> Text -> Text -> Text
transform autorequireEnabled filename imports input
  | autorequireEnabled = transform' True filename imports input
  | noAutorequire = transform' False filename imports input
  | otherwise = transform' True filename imports input
  where
    noAutorequire = not (any ("autorequire" `Text.isPrefixOf`) $ lines input)

transform' :: Bool -> FileName -> Text -> Text -> Text
transform' shouldPrepend filename prepended input =
  Text.lines input
    & filter (\t -> not $ "autorequire" `Text.isPrefixOf` t)
    & zip [1 ..]
    >>= prependAfterModuleLine
    <&> (\(ln, text) -> maybe (text <> "\n") (renderImport filename (LineNumber ln)) $ Megaparsec.parseMaybe requireParser text)
    & (lineTag filename (LineNumber 1) :)
    & Text.concat
  where
    enumeratedPrepend ln
      | shouldPrepend = zip (repeat ln) (Text.lines prepended)
      | otherwise = []
    prependAfterModuleLine (ln, text)
      | ("module" `Text.isPrefixOf` text)
          && ("where" `Text.isSuffixOf`) text =
        (ln, text) : enumeratedPrepend ln
      | ("instance" `Text.isPrefixOf` text)
          && ("where" `Text.isSuffixOf`) text =
        [(ln, text)]
      | ("data" `Text.isPrefixOf` text)
          && ("where" `Text.isSuffixOf`) text =
        [(ln, text)]
      | ("class" `Text.isPrefixOf` text)
          && ("where" `Text.isSuffixOf`) text =
        [(ln, text)]
      | not ("instance" `Text.isPrefixOf` text)
          && not ("class" `Text.isPrefixOf` text)
          && not ("data" `Text.isPrefixOf` text)
          && ("where" `Text.isPrefixOf`) text =
        (ln, text) : enumeratedPrepend ln
      | otherwise = [(ln, text)]

lineTag :: FileName -> LineNumber -> Text
lineTag (FileName fn) (LineNumber ln) =
  "{-# LINE "
    <> show ln
    <> " \""
    <> fn
    <> "\" #-}\n"

renderImport :: FileName -> LineNumber -> RequireInfo -> Text
renderImport filename linenumber RequireInfo {..} =
  if Text.isInfixOf riFullModuleName (unFileName filename)
    then ""
    else typesImport <> lineTag filename linenumber <> qualifiedImport
  where
    types = maybe (Text.takeWhileEnd (/= '.') riFullModuleName) (Text.intercalate ",") riImportedTypes
    typesImport = "import " <> riFullModuleName <> " (" <> types <> ")\n"
    qualifiedImport = "import qualified " <> riFullModuleName <> " as " <> riModuleAlias <> "\n"

requireParser :: Parser RequireInfo
requireParser = do
  void $ Megaparsec.string "require"
  void Megaparsec.space1
  module' <- Megaparsec.some (Megaparsec.alphaNumChar <|> Megaparsec.punctuationChar)
  void Megaparsec.space
  alias' <- Megaparsec.try $ Megaparsec.option Nothing $ do
    void $ Megaparsec.string "as"
    void Megaparsec.space1
    Just <$> Megaparsec.some Megaparsec.alphaNumChar
  void Megaparsec.space
  types' <- Megaparsec.option Nothing $ do
    void $ Megaparsec.char '('
    t' <- Megaparsec.many (Megaparsec.alphaNumChar <|> Megaparsec.char ',' <|> Megaparsec.char ' ')
    void $ Megaparsec.char ')'
    return $ Just t'
  void $ Megaparsec.option Nothing $ do
    void Megaparsec.space
    void $ Megaparsec.some (Megaparsec.char '-')
    void $ Megaparsec.many (Megaparsec.alphaNumChar <|> Megaparsec.char ' ')
    return Nothing
  return
    RequireInfo
      { riFullModuleName = toText module',
        riModuleAlias = maybe (Text.takeWhileEnd (/= '.') $ toText module') toText alias',
        riImportedTypes = (fmap Text.strip <$> Text.splitOn ",") . toText <$> types'
      }