{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}

-- | Data types and functions for representing a simplified form of PureScript
-- code, intended for use in e.g. HTML documentation.

module Language.PureScript.Docs.RenderedCode.Types
 ( RenderedCodeElement(..)
 , asRenderedCodeElement
 , ContainingModule(..)
 , asContainingModule
 , containingModuleToMaybe
 , maybeToContainingModule
 , fromContainingModule
 , RenderedCode
 , asRenderedCode
 , outputWith
 , sp
 , syntax
 , ident
 , ctor
 , kind
 , keyword
 , keywordForall
 , keywordData
 , keywordNewtype
 , keywordType
 , keywordClass
 , keywordInstance
 , keywordWhere
 ) where

import Control.Applicative ((<$>), (<*>), (*>), pure)
import Data.Foldable
import Data.Monoid
import qualified Data.Aeson as A
import Data.Aeson.BetterErrors
import Control.Monad.Error.Class (MonadError(..))

import qualified Language.PureScript as P

-- |
-- A single element in a rendered code fragment. The intention is to support
-- multiple output formats. For example, plain text, or highlighted HTML.
--
data RenderedCodeElement
  = Syntax String
  | Ident String
  | Ctor String ContainingModule
  | Kind String
  | Keyword String
  | Space
  deriving (Show, Eq, Ord)

instance A.ToJSON RenderedCodeElement where
  toJSON (Syntax str) =
    A.toJSON ["syntax", str]
  toJSON (Ident str) =
    A.toJSON ["ident", str]
  toJSON (Ctor str mn) =
    A.toJSON ["ctor", A.toJSON str, A.toJSON mn ]
  toJSON (Kind str) =
    A.toJSON ["kind", str]
  toJSON (Keyword str) =
    A.toJSON ["keyword", str]
  toJSON Space =
    A.toJSON ["space" :: String]

asRenderedCodeElement :: Parse String RenderedCodeElement
asRenderedCodeElement =
  a Syntax "syntax" <|>
  a Ident "ident" <|>
  asCtor <|>
  a Kind "kind" <|>
  a Keyword "keyword" <|>
  asSpace <|>
  unableToParse
  where
  p <|> q = catchError p (const q)

  a ctor' ctorStr = ctor' <$> (nth 0 (withString (eq ctorStr)) *> nth 1 asString)
  asCtor = nth 0 (withString (eq "ctor")) *> (Ctor <$> nth 1 asString <*> nth 2 asContainingModule)
  asSpace = nth 0 (withString (eq "space")) *> pure Space

  eq s s' = if s == s' then Right () else Left ""

  unableToParse = withString (Left . show)

-- |
-- This type is isomorphic to 'Maybe' 'P.ModuleName'. It makes code a bit easier
-- to read, as the meaning is more explicit.
--
data ContainingModule
  = ThisModule
  | OtherModule P.ModuleName
  deriving (Show, Eq, Ord)

instance A.ToJSON ContainingModule where
  toJSON mn = A.toJSON (P.runModuleName <$> containingModuleToMaybe mn)

asContainingModule :: Parse e ContainingModule
asContainingModule =
  maybeToContainingModule <$> perhaps (P.moduleNameFromString <$> asString)

-- |
-- Convert a 'Maybe' 'P.ModuleName' to a 'ContainingModule', using the obvious
-- isomorphism.
--
maybeToContainingModule :: Maybe P.ModuleName -> ContainingModule
maybeToContainingModule Nothing = ThisModule
maybeToContainingModule (Just mn) = OtherModule mn

-- |
-- Convert a 'ContainingModule' to a 'Maybe' 'P.ModuleName', using the obvious
-- isomorphism.
--
containingModuleToMaybe :: ContainingModule -> Maybe P.ModuleName
containingModuleToMaybe ThisModule = Nothing
containingModuleToMaybe (OtherModule mn) = Just mn

-- |
-- A version of 'fromMaybe' for 'ContainingModule' values.
--
fromContainingModule :: P.ModuleName -> ContainingModule -> P.ModuleName
fromContainingModule def ThisModule = def
fromContainingModule _ (OtherModule mn) = mn

-- |
-- A type representing a highly simplified version of PureScript code, intended
-- for use in output formats like plain text or HTML.
--
newtype RenderedCode
  = RC { unRC :: [RenderedCodeElement] }
  deriving (Show, Eq, Ord, Monoid)

instance A.ToJSON RenderedCode where
  toJSON (RC elems) = A.toJSON elems

asRenderedCode :: Parse String RenderedCode
asRenderedCode = RC <$> eachInArray asRenderedCodeElement

-- |
-- This function allows conversion of a 'RenderedCode' value into a value of
-- some other type (for example, plain text, or HTML). The first argument
-- is a function specifying how each individual 'RenderedCodeElement' should be
-- rendered.
--
outputWith :: Monoid a => (RenderedCodeElement -> a) -> RenderedCode -> a
outputWith f = foldMap f . unRC

-- |
-- A 'RenderedCode' fragment representing a space.
--
sp :: RenderedCode
sp = RC [Space]

syntax :: String -> RenderedCode
syntax x = RC [Syntax x]

ident :: String -> RenderedCode
ident x = RC [Ident x]

ctor :: String -> ContainingModule -> RenderedCode
ctor x m = RC [Ctor x m]

kind :: String -> RenderedCode
kind x = RC [Kind x]

keyword :: String -> RenderedCode
keyword kw = RC [Keyword kw]

keywordForall :: RenderedCode
keywordForall = keyword "forall"

keywordData :: RenderedCode
keywordData = keyword "data"

keywordNewtype :: RenderedCode
keywordNewtype = keyword "newtype"

keywordType :: RenderedCode
keywordType = keyword "type"

keywordClass :: RenderedCode
keywordClass = keyword "class"

keywordInstance :: RenderedCode
keywordInstance = keyword "instance"

keywordWhere :: RenderedCode
keywordWhere = keyword "where"