{-# OPTIONS_GHC -fno-warn-orphans #-}

-- |
--
-- Copyright:
--   This file is part of the package addy. It is subject to the license
--   terms in the LICENSE file found in the top-level directory of this
--   distribution and at:
--
--     https://code.devalot.com/open/addy
--
--   No part of this package, including this file, may be copied,
--   modified, propagated, or distributed except according to the terms
--   contained in the LICENSE file.
--
-- License: BSD-2-Clause
--
-- Internal functions to render an 'EmailAddr' to a 'TB.Builder'.
module Addy.Internal.Render
  ( Mode (..),
    render,
    renderToText,
    renderAddrSpec,
    renderDisplayName,
    renderComments,
  )
where

import Addy.Internal.Char
import Addy.Internal.Types
import qualified Data.Text as Text
import qualified Data.Text.Lazy.Builder as TB
import qualified Net.IP as IP
import Text.Show (Show (..), showParen, showString)
import Prelude hiding (show)

-- | Render mode.
--
-- @since 0.1.0.0
data Mode
  = -- | Render the entire email address, including the optional
    -- display name and comments.
    Full
  | -- | Only render the simplest form of the email address.  Only the
    -- 'LocalPart' and 'Domain' are rendered in this mode.
    Short

-- | Render an email address.
--
-- @since 0.1.0.0
render :: Mode -> EmailAddr -> TB.Builder
render = \case
  Short ->
    renderAddrSpec Short
  Full -> \addr@EmailAddr {..} ->
    case _displayName of
      Nothing ->
        renderAddrSpec Full addr
          <> renderComments Full AfterAddress _comments
      Just name ->
        mconcat
          [ renderComments Full BeforeDisplayName _comments,
            renderDisplayName name <> TB.singleton ' ',
            renderComments Full AfterDisplayName _comments,
            TB.singleton '<' <> renderAddrSpec Full addr <> TB.singleton '>',
            renderComments Full AfterAddress _comments
          ]

-- | Render an email address in @addr-spec@ format.
--
-- @since 0.1.0.0
renderAddrSpec :: Mode -> EmailAddr -> TB.Builder
renderAddrSpec mode EmailAddr {..} =
  mconcat
    [ renderComments mode BeforeLocalPart _comments,
      lp _localPart <> TB.singleton '@' <> dn _domain,
      renderComments mode AfterDomain _comments
    ]
  where
    lp :: LocalPart -> TB.Builder
    lp (LP t)
      | mustQuoteLocalPart t = wrap '"' '"' t
      | otherwise = TB.fromText t
    dn :: Domain -> TB.Builder
    dn = \case
      Domain (DN t) ->
        TB.fromText t
      DomainLiteral lit ->
        wrap '[' ']' $ case lit of
          IpAddressLiteral ip ->
            if IP.isIPv6 ip
              then "IPv6:" <> IP.encode ip
              else IP.encode ip
          TaggedAddressLiteral (AT tag) (Lit body) ->
            tag <> ":" <> body
          AddressLiteral (Lit body) ->
            body

-- | Render a display name.
--
-- @since 0.1.0.0
renderDisplayName :: DisplayName -> TB.Builder
renderDisplayName (DP t)
  | Text.all (\c -> atext c || wsp c) t =
    TB.fromText t
  | otherwise =
    wrap '"' '"' t

-- | Render comments that have the given 'CommentLoc'.  The comment
-- location is also used to decide where to introduce white space.
--
-- @since 0.1.0.0
renderComments :: Mode -> CommentLoc -> [Comment] -> TB.Builder
renderComments Short _ _ = mempty
renderComments Full loc cs =
  case go (== loc) cs of
    Nothing -> mempty
    Just tb -> case loc of
      BeforeDisplayName -> TB.singleton ' ' <> tb
      AfterDisplayName -> tb <> TB.singleton ' '
      BeforeLocalPart -> tb <> TB.singleton ' '
      AfterDomain -> TB.singleton ' ' <> tb
      AfterAddress -> TB.singleton ' ' <> tb
  where
    go :: (CommentLoc -> Bool) -> [Comment] -> Maybe TB.Builder
    go f cs =
      filter (\(Comment loc (CC t)) -> f loc && not (Text.null t)) cs
        & map (\(Comment _ (CC t)) -> t)
        & Text.intercalate " "
        & \t ->
          if Text.null t
            then Nothing
            else Just $ wrap '(' ')' t

-- | Render the given address as text.
--
-- @since 0.1.0.0
renderToText :: Mode -> EmailAddr -> Text
renderToText m =
  render m
    >>> TB.toLazyText
    >>> toStrict

-- | Wrap and quote some text.
--
-- @since 0.1.0.0
wrap :: Char -> Char -> Text -> TB.Builder
wrap lh rh t =
  mconcat
    [ TB.singleton lh,
      Text.foldl' escape mempty t,
      TB.singleton rh
    ]
  where
    escape :: TB.Builder -> Char -> TB.Builder
    escape tb c
      | c == lh || c == rh || c == '\\' =
        tb <> TB.singleton '\\' <> TB.singleton c
      | otherwise =
        tb <> TB.singleton c

-- | 'True' if the give text, when used as the @local-part@ of an
-- email address must be wrapped in quotation marks.
--
-- @since 0.1.0.0
mustQuoteLocalPart :: Text -> Bool
mustQuoteLocalPart name =
  Text.any
    ( \c ->
        c == '"'
          || c == '\\'
          || c == ')'
          || c == '('
          || c == '@'
          || wsp c
    )
    name
    || Text.isPrefixOf "." name
    || Text.isSuffixOf "." name
    || Text.isInfixOf ".." name
    || Text.null name -- Yes, this is totally legit.

-- Orphan instance that renders the email address.
instance Show EmailAddr where
  showsPrec d addr =
    showParen (d > 10) $
      showString "EmailAddr " . showsPrec d (render Full addr)