{-# LANGUAGE GADTs             #-}
{-# LANGUAGE NamedFieldPuns    #-}
{-# LANGUAGE OverloadedStrings #-}

-- | 'bytestring''s 'Builder' for a 'Tag'
--
module GhcTags.CTag.Formatter
  ( formatTagsFile
  -- * format a ctag
  , formatTag
  -- * format a pseudo-ctag
  , formatHeader
  ) where

import           Control.Arrow ((|||))
import           Data.ByteString.Builder (Builder)
import qualified Data.ByteString.Builder as BS
import           Data.Char (isAscii)
import           Data.Text          (Text)
import qualified Data.Text          as Text
import qualified Data.Text.Encoding as Text

import           GhcTags.Tag
import           GhcTags.Utils (endOfLine)
import           GhcTags.CTag.Header
import           GhcTags.CTag.Utils


-- | 'ByteString' 'Builder' for a single line.
--
formatTag :: CTag -> Builder
formatTag Tag { tagName, tagFilePath, tagAddr, tagKind, tagFields = TagFields tagFields } =

       (BS.byteString . Text.encodeUtf8 . getTagName $ tagName)
    <> BS.charUtf8 '\t'

    <> (BS.byteString . Text.encodeUtf8 . Text.pack $ tagFilePath)
    <> BS.charUtf8 '\t'

    <> formatTagAddress tagAddr
    -- we are using extended format: '_TAG_FILE_FROMAT	2'
    <> BS.stringUtf8 ";\""

    -- tag kind: we are encoding them using field syntax: this is because vim
    -- is using them in the right way: https://github.com/vim/vim/issues/5724
    <> formatKindChar tagKind

    -- tag fields
    <> foldMap ((BS.charUtf8 '\t' <>) . formatField) tagFields

    <> BS.stringUtf8 endOfLine

  where

    formatTagAddress :: CTagAddress -> Builder
    formatTagAddress (TagLineCol lineNo _colNo) =
      BS.intDec lineNo -- Vim only allows to use ranges; there's no way to
                       -- specify column (`c|` command is not allowed)
    formatTagAddress (TagLine lineNo) =
      BS.intDec lineNo
    formatTagAddress (TagCommand exCommand) =
      BS.byteString . Text.encodeUtf8 . getExCommand $ exCommand

    formatKindChar :: CTagKind -> Builder
    formatKindChar tk =
      case tagKindToChar tk of
        Nothing -> mempty
        Just c | isAscii c -> BS.charUtf8 '\t' <> BS.charUtf8 c
               | otherwise -> BS.stringUtf8 "\tkind:" <> BS.charUtf8 c


formatField :: TagField -> Builder
formatField TagField { fieldName, fieldValue } =
      BS.byteString (Text.encodeUtf8 fieldName)
   <> BS.charUtf8 ':'
   <> BS.byteString (Text.encodeUtf8 fieldValue)


formatHeader :: Header -> Builder
formatHeader Header { headerType, headerLanguage, headerArg, headerComment } =
    case headerType of
      FileEncoding ->
        formatTextHeaderArgs "FILE_ENCODING"     headerLanguage headerArg headerComment
      FileFormat ->
        formatIntHeaderArgs "FILE_FORMAT"        headerLanguage headerArg headerComment
      FileSorted ->
        formatIntHeaderArgs "FILE_SORTED"        headerLanguage headerArg headerComment
      OutputMode ->
        formatTextHeaderArgs "OUTPUT_MODE"       headerLanguage headerArg headerComment
      KindDescription ->
        formatTextHeaderArgs "KIND_DESCRIPTION"  headerLanguage headerArg headerComment
      KindSeparator ->
        formatTextHeaderArgs "KIND_SEPARATOR"    headerLanguage headerArg headerComment
      ProgramAuthor ->
        formatTextHeaderArgs "PROGRAM_AUTHOR"    headerLanguage headerArg headerComment
      ProgramName ->
        formatTextHeaderArgs "PROGRAM_NAME"      headerLanguage headerArg headerComment
      ProgramUrl ->
        formatTextHeaderArgs "PROGRAM_URL"       headerLanguage headerArg headerComment
      ProgramVersion ->
        formatTextHeaderArgs "PROGRAM_VERSION"   headerLanguage headerArg headerComment
      ExtraDescription ->
        formatTextHeaderArgs "EXTRA_DESCRIPTION" headerLanguage headerArg headerComment
      FieldDescription ->
        formatTextHeaderArgs "FIELD_DESCRIPTION" headerLanguage headerArg headerComment
      PseudoTag name ->
        formatHeaderArgs (BS.byteString . Text.encodeUtf8)
                         "!_" name headerLanguage headerArg headerComment
  where
    formatHeaderArgs :: (ty -> Builder)
                     -> String
                     -> Text
                     -> Maybe Text
                     -> ty
                     -> Text
                     -> Builder
    formatHeaderArgs formatArg prefix headerName language arg comment =
         BS.stringUtf8 prefix
      <> BS.byteString (Text.encodeUtf8 headerName)
      <> foldMap ((BS.charUtf8 '!' <>) . BS.byteString . Text.encodeUtf8) language
      <> BS.charUtf8 '\t'
      <> formatArg arg
      <> BS.stringUtf8 "\t/"
      <> BS.byteString (Text.encodeUtf8 comment)
      <> BS.charUtf8 '/'
      <> BS.stringUtf8 endOfLine

    formatTextHeaderArgs = formatHeaderArgs (BS.byteString . Text.encodeUtf8) "!_TAG_"
    formatIntHeaderArgs  = formatHeaderArgs BS.intDec "!_TAG_"


-- | 'ByteString' 'Builder' for vim 'Tag' file.
--
formatTagsFile :: [Either Header CTag]             -- ^ 'CTag's
               -> Builder
formatTagsFile tags =
    foldMap (formatHeader ||| formatTag) tags