{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE LambdaCase #-}

module Data.Apiary.Document.Internal where

import Control.Applicative((<$>))

import Data.Typeable(TypeRep)
import Data.Maybe(mapMaybe)
import Data.List(groupBy)
import Data.Function(on)

import Data.Apiary.Param(StrategyRep, QueryRep)
import Data.Apiary.Method(Method)

import Text.Blaze.Html(Html)
import qualified Data.Text as T
import qualified Data.ByteString as S

data Doc
    = DocPath   T.Text       Doc
    | DocRoot                Doc
    | DocFetch  T.Text TypeRep (Maybe Html) Doc
    | DocRest   T.Text  (Maybe Html) Doc
    | DocAny    Doc
    | DocDropNext            Doc

    | DocMethod Method       Doc
    | DocQuery  T.Text StrategyRep QueryRep (Maybe Html) Doc
    | DocPrecondition Html   Doc
    | DocAccept S.ByteString Doc
    | DocGroup  T.Text       Doc
    | Document  T.Text       Doc
    | Action

--------------------------------------------------------------------------------

data Route
    = Path  T.Text                      Route
    | Fetch T.Text TypeRep (Maybe Html) Route
    | Rest  T.Text (Maybe Html) -- ^ \*\* with name
    | Any -- ^ \*\* without name
    | End

instance Eq Route where
    Path    a   d == Path  b   d'   = a == b && d == d'
    Fetch k a _ d == Fetch l b _ d' = k == l && a == b && d == d'
    Rest  k _     == Rest  l _      = k == l
    Any           == Any            = True
    End           == End            = True
    _             == _              = False

data Documents = Documents
    { noGroup :: [PathDoc]
    , groups  :: [(T.Text, [PathDoc])]
    }

data PathDoc = PathDoc
    { path    :: Route
    , methods :: [(Method, [MethodDoc])]
    }

-- | query parameters document
data QueryDoc = QueryDoc
    { queryName     :: T.Text
    , queryStrategy :: StrategyRep
    , queryRep      :: QueryRep
    , queryDocument :: (Maybe Html)
    }

data MethodDoc = MethodDoc
    { queries       :: [QueryDoc]
    , preconditions :: [Html]
    , accept        :: Maybe S.ByteString
    , document      :: T.Text
    }

--------------------------------------------------------------------------------

data ToDocumentState = ToDocumentState
    { toDocumentPath      :: Route -> Route
    , toDocumentMethodDoc :: [MethodDoc] -> [(Method, [MethodDoc])]
    , toDocumentQueries   :: [QueryDoc] -> [QueryDoc]
    , toDocumentPreconds  :: [Html] -> [Html]
    , toDocumentAccept    :: Maybe S.ByteString
    , toDocumentDocument  :: Maybe T.Text
    }

initialToDocumentState :: ToDocumentState
initialToDocumentState = ToDocumentState id (\md -> [("*", md)]) id id Nothing Nothing

docToDocument :: Doc -> Maybe (Maybe T.Text, PathDoc)
docToDocument = \case
    (DocGroup "" d') -> (Nothing,) <$> loop initialToDocumentState d'
    (DocGroup g  d') -> (Just  g,) <$> loop initialToDocumentState d'
    d'               -> (Nothing,) <$> loop initialToDocumentState d'
  where
    loop st (DocDropNext       d) = loop st (dropNext d)
    loop st (DocPath         p d) = loop st { toDocumentPath = toDocumentPath st . Path p } d
    loop st (DocRoot           d) = loop st { toDocumentPath = const $ Path "" End } d
    loop st (DocFetch    k t h d) = loop st { toDocumentPath = toDocumentPath st . Fetch k t h } d
    loop st (DocRest     k   h d) = loop st { toDocumentPath = toDocumentPath st . const (Rest k h) } d
    loop st (DocAny            d) = loop st { toDocumentPath = toDocumentPath st . const Any } d
    loop st (DocMethod       m d) = loop st { toDocumentMethodDoc = (\md -> [(m, md)]) } d
    loop st (DocQuery  p s q t d) = loop st { toDocumentQueries = toDocumentQueries st . (QueryDoc p s q t:) } d
    loop st (DocPrecondition h d) = loop st { toDocumentPreconds = toDocumentPreconds st . (h:) } d
    loop st (DocGroup        _ d) = loop st d
    loop st (DocAccept       a d) = loop st { toDocumentAccept = Just a } d
    loop st (Document        t d) = loop st { toDocumentDocument = Just t} d
    loop st Action                = case toDocumentDocument st of
        Nothing  -> Nothing
        Just doc -> Just . PathDoc (toDocumentPath st End) $ toDocumentMethodDoc st
            [MethodDoc (toDocumentQueries st []) (toDocumentPreconds st []) (toDocumentAccept st) doc]

    dropNext (DocPath         _ d) = d
    dropNext (DocRoot           d) = d
    dropNext (DocFetch    _ _ _ d) = d
    dropNext (DocRest       _ _ d) = d
    dropNext (DocAny            d) = d
    dropNext (DocDropNext       d) = dropNext d
    dropNext (DocMethod       _ d) = d
    dropNext (DocQuery  _ _ _ _ d) = d
    dropNext (DocPrecondition _ d) = d
    dropNext (DocGroup        _ d) = d
    dropNext (DocAccept       _ d) = d
    dropNext (Document        _ d) = d
    dropNext Action                = Action

mergePathDoc :: [PathDoc] -> [PathDoc]
mergePathDoc [] = []
mergePathDoc (pd:pds) = merge (filter (same pd) pds) : mergePathDoc (filter (not . same pd) pds)
  where
    same           = (==) `on` path
    merge pds' = PathDoc (path pd) (mergeMethods $ methods pd ++ concatMap methods pds')

mergeMethods :: [(Method, [MethodDoc])] -> [(Method, [MethodDoc])]
mergeMethods [] = []
mergeMethods (m:ms) = merge (filter (same m) ms) : mergeMethods (filter (not . same m) ms)
  where
    same = (==) `on` fst
    merge ms' = (fst m, snd m ++ concatMap snd ms')


docsToDocuments :: [Doc] -> Documents
docsToDocuments doc =
    let gds = mapMaybe docToDocument doc
        ngs = mergePathDoc . map snd $ filter ((Nothing ==) . fst)   gds
        gs  = map upGroup . groupBy ((==) `on` fst) $ mapMaybe trav gds
    in Documents ngs gs
  where
    upGroup ((g,d):ig) = (g, mergePathDoc $ d : map snd ig)
    upGroup []         = error "docsToDocuments: unknown error."

    trav (Nothing, _) = Nothing
    trav (Just a,  b) = Just (a, b)