{-# LANGUAGE BangPatterns               #-}
{-# LANGUAGE DataKinds                  #-}
{-# LANGUAGE LambdaCase                 #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE DerivingStrategies         #-}
{-# LANGUAGE NamedFieldPuns             #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE ScopedTypeVariables        #-}

-- | Parser combinators for etags file format
--
module GhcTags.ETag.Parser
  ( parseTagsFile
  , parseTagFileSection
  , parseTag
  ) where

import           Control.Applicative (many, (<|>))
import           Data.ByteString (ByteString)
import           Data.Attoparsec.ByteString  (Parser, (<?>))
import qualified Data.Attoparsec.ByteString  as AB
import qualified Data.Attoparsec.ByteString.Char8  as AChar
import           Data.Functor (($>))
import           Data.Text (Text)
import qualified Data.Text.Encoding as Text
import qualified System.FilePath.ByteString as FilePath

import           GhcTags.Tag
import qualified GhcTags.Utils as Utils


-- | Parse whole etags file
--
parseTagsFile :: ByteString
              -> IO (Either String [ETag])
parseTagsFile =
      fmap AB.eitherResult
    . AB.parseWith (pure mempty)
                   (concat <$> many parseTagFileSection)


-- | Parse tags from a single file (a single section in etags file).
--
parseTagFileSection :: Parser [ETag]
parseTagFileSection = do
      tagFilePath <-
        AChar.char '\x0c' *> endOfLine
                          *> parseTagFilePath
      many (parseTag tagFilePath)

parseTagFilePath :: Parser TagFilePath
parseTagFilePath =
      TagFilePath . Text.decodeUtf8 . FilePath.normalise
  <$> AChar.takeWhile (\x -> x /= ',' && Utils.notNewLine x)
  <*  AChar.char ','
  <*  (AChar.decimal :: Parser Int)
  <*  endOfLine
  <?> "parsing tag file name failed"


-- | Parse an 'ETag' from a single line.
--
parseTag :: TagFilePath -> Parser ETag
parseTag tagFilePath =
          mkTag
      <$> parseTagDefinition
      <*> ((Just <$> parseTagName) <|> pure Nothing)
      <*> AChar.decimal
      <*  AChar.char ','
      <*> AChar.decimal
      <*  endOfLine
      <?> "parsing tag failed"
  where
    mkTag :: Text -> Maybe TagName -> Int -> Int -> ETag
    mkTag tagDefinition mTagName lineNo byteOffset =
      Tag { tagName       = case mTagName of
                              Nothing   -> TagName tagDefinition
                              Just name -> name
          , tagKind       = NoKind
          , tagFilePath
          , tagAddr       = TagLineCol lineNo byteOffset
          , tagDefinition = case mTagName of
                              Nothing -> NoTagDefinition
                              Just _  -> TagDefinition tagDefinition
          , tagFields     = NoTagFields
          }

    parseTagName :: Parser TagName
    parseTagName =
          TagName . Text.decodeUtf8
      <$> AChar.takeWhile (\x -> x /= '\SOH' && Utils.notNewLine x)
      <*  AChar.char '\SOH'
      <?> "parsing tag name failed"

    parseTagDefinition :: Parser Text
    parseTagDefinition =
          Text.decodeUtf8
      <$> AChar.takeWhile (\x -> x /= '\DEL' && Utils.notNewLine x)
      <*  AChar.char '\DEL'
      <?> "parsing tag definition failed"

endOfLine :: Parser ()
endOfLine = AChar.string "\r\n" $> ()
        <|> AChar.char '\r' $> ()
        <|> AChar.char '\n' $> ()