module Docs (
  ApiDoc,
  ModuleDoc(..),
  noModuleDoc,
  DocSection(..),
  FuncDoc(..),
  ParamDoc(..),
  PropDoc(..),
  SignalDoc(..),
  DocPara(..),
  DocParaSpan(..),
  extractDocumentation
  ) where

import qualified Text.XML.HaXml as Xml

import Prelude hiding (elem)
import Data.Char (isUpper, isSpace)
import Data.List (partition, groupBy)
import Data.Tree (Forest, unfoldForest)

-------------------------------------------------------------------------------
-- Types representing the content of the documentation XML file
-------------------------------------------------------------------------------
type ApiDoc = [ModuleDoc]

data ModuleDoc = ModuleDoc {
    moduledoc_name :: String,              -- these docs apply to this object
    moduledoc_altname :: String,           -- sometimes a better index entry
    moduledoc_summary :: [DocPara],        -- usually a one line summary
    moduledoc_description :: [DocPara],    -- the main description
    moduledoc_sections :: [DocSection],    -- any additional titled subsections
    moduledoc_hierarchy :: Forest String,  -- a tree of parent objects (as text)
    moduledoc_functions :: [FuncDoc],      -- documentation for each function
    moduledoc_callbacks :: [FuncDoc],      -- documentation for callback types
    moduledoc_properties :: [PropDoc],     -- documentation for each property
    moduledoc_childprops :: [PropDoc],     -- documentation for each child property
    moduledoc_signals :: [SignalDoc],      -- documentation for each signal
    moduledoc_since :: String              -- which version of the api the
  }                                        -- module is available from, eg "2.4"

noModuleDoc :: ModuleDoc
noModuleDoc = ModuleDoc {
    moduledoc_name = "",
    moduledoc_altname = "",
    moduledoc_summary = [],
    moduledoc_description = [],
    moduledoc_sections = [],
    moduledoc_hierarchy = [],
    moduledoc_functions = [],
    moduledoc_callbacks = [],
    moduledoc_properties = [],
    moduledoc_childprops = [],
    moduledoc_signals = [],
    moduledoc_since = ""
  }

data DocSection = DocSection {
    section_title :: String,
    section_paras :: [DocPara]
  }

type Since = String

data FuncDoc = FuncDoc {
    funcdoc_name :: String,             -- C function name
    funcdoc_paragraphs :: [DocPara],    -- documentation markup
    funcdoc_params :: [ParamDoc],       -- parameter documentation
    funcdoc_since :: Since              -- which version of the api the
  }                                     -- function is available from, eg "2.4"

data ParamDoc = ParamDoc {
    paramdoc_name :: String,            -- parameter name or "Returns"
    paramdoc_paragraph :: [DocParaSpan] -- a simple paragraph
  }

data PropDoc = PropDoc {
    propdoc_name :: String,             -- property name
    propdoc_paragraphs :: [DocPara],    -- documentation markup
    propdoc_since :: Since              -- which version of the api the
  }                                     -- function is available from, eg "2.4"

data SignalDoc = SignalDoc {
    signaldoc_name :: String,           -- C signal name
    signaldoc_paragraphs :: [DocPara],  -- documentation markup
    signaldoc_params :: [ParamDoc],     -- parameter documentation
    signaldoc_since :: Since            -- which version of the api the
  }                                     -- function is available from, eg "2.4"

data DocPara =
    DocParaText [DocParaSpan]           -- an ordinary word-wrapped paragraph
  | DocParaProgram String               -- a verbatim section
  | DocParaTitle String                 -- a title to a subsection eg an example
  | DocParaDefItem [DocParaSpan] [DocParaSpan] -- a definition list item
  | DocParaListItem [DocParaSpan]       -- a itemisted list item

data DocParaSpan = DocText String       -- just simple text
                 | DocFuncXRef String   -- cross reference to a function name
                 | DocTypeXRef String   -- cross reference to a type name
                 | DocOtherXRef String  -- xref format not directly supported
                 | DocEmphasis String   -- emphasised text, usually italic
                 | DocLiteral String    -- some literal like numbers
                 | DocArg  String       -- function argument names


-------------------------------------------------------------------------------
-- extract functions to convert the doc xml file to the internal representation
-------------------------------------------------------------------------------
extractDocumentation :: Xml.Document -> ApiDoc
extractDocumentation (Xml.Document _ _ (Xml.Elem "apidoc" [] modules) _) =
  map extractDocModule (concatMap (Xml.foldXml white) modules)
  where
  -- remove empty CString constructors from the whole document
  white :: Xml.CFilter
  white (Xml.CString False str) | all isSpace str = []
  white elem = [elem]

extractDocModule :: Xml.Content -> ModuleDoc
extractDocModule (Xml.CElem (Xml.Elem "module" [] (moduleinfo:rest))) =
  let functions = [ e | e@(Xml.CElem (Xml.Elem "function" _ _)) <- rest ]
      properties = [ e | e@(Xml.CElem (Xml.Elem "property" _ _)) <- rest ]
      childprops = [ e | e@(Xml.CElem (Xml.Elem "childprop" _ _)) <- rest ]
      signals = [ e | e@(Xml.CElem (Xml.Elem "signal" _ _)) <- rest ]
      (callbacks, functions') = partition (isUpper.head.funcdoc_name)
                                         (map extractDocFunc functions)
  in (extractDocModuleinfo moduleinfo) {
    moduledoc_functions = functions',
    moduledoc_callbacks = callbacks,
    moduledoc_properties = map extractDocProp properties,
    moduledoc_childprops = map extractDocChildProp childprops,
    moduledoc_signals = map extractDocSignal signals
  }

extractDocModuleinfo :: Xml.Content -> ModuleDoc
extractDocModuleinfo
  (Xml.CElem (Xml.Elem "module-info" []
    [Xml.CElem (Xml.Elem "name" [] name)
    ,Xml.CElem (Xml.Elem "altname" [] altname)
    ,Xml.CElem (Xml.Elem "summary" [] summary)
    ,Xml.CElem (Xml.Elem "description" [] parasAndSections)
    ,Xml.CElem (Xml.Elem "object-hierarchy" [] objHierSpans)]
  )) =
  let (paras, sections) = span (\elem ->
        case elem of
          Xml.CElem (Xml.Elem "section" _ _) -> False
          _ -> True) parasAndSections
   in ModuleDoc {
    moduledoc_name = Xml.verbatim name,
    moduledoc_altname = Xml.verbatim altname,
    moduledoc_summary = [DocParaText (map extractDocParaSpan summary)],
    moduledoc_description = concatMap extractDocPara paras,
    moduledoc_sections = map extractDocSection sections,
    moduledoc_hierarchy = parseHierarchyDocs
                            (map extractDocParaSpan objHierSpans),
    moduledoc_functions = undefined,
    moduledoc_callbacks = undefined,
    moduledoc_properties = undefined,
    moduledoc_childprops = undefined,
    moduledoc_signals = undefined,
    moduledoc_since = ""
  }

extractDocSection :: Xml.Content -> DocSection
extractDocSection
  (Xml.CElem (Xml.Elem "section" []
    (Xml.CElem (Xml.Elem "title" [] [Xml.CString _ title])
    :paras))) =
  DocSection {
    section_title = title,
    section_paras = concatMap extractDocPara paras
  }
extractDocSection other = error $ "extractDocSection: " ++ Xml.verbatim other

extractDocFunc :: Xml.Content -> FuncDoc
extractDocFunc
  (Xml.CElem (Xml.Elem "function" []
    [Xml.CElem (Xml.Elem "name" [] [Xml.CString _ name])
    ,Xml.CElem (Xml.Elem "since" [] since')
    ,Xml.CElem (Xml.Elem "doc" [] paras)
    ,Xml.CElem (Xml.Elem "params" [] params)]
  )) =
  let since = case since' of
                [] -> ""
                [Xml.CString _ since''] | last since'' == '.' -> init since''
                                        | otherwise           -> since''
   in FuncDoc {
        funcdoc_name = name,
        funcdoc_paragraphs = concatMap extractDocPara paras,
        funcdoc_params = map extractParamDoc params,
        funcdoc_since = since
      }

extractParamDoc :: Xml.Content -> ParamDoc
extractParamDoc
  (Xml.CElem (Xml.Elem "param" []
    (Xml.CElem (Xml.Elem "name" [] [Xml.CString _ name])
    :spans))) =
  ParamDoc {
    paramdoc_name = name,
    paramdoc_paragraph = map extractDocParaSpan spans
  }

extractDocProp :: Xml.Content -> PropDoc
extractDocProp
  (Xml.CElem (Xml.Elem "property" []
    [Xml.CElem (Xml.Elem "name" [] [Xml.CString _ name])
    ,Xml.CElem (Xml.Elem "since" [] since')
    ,Xml.CElem (Xml.Elem "doc" [] paras)]
  )) =
  let since = case since' of
                [] -> ""
                [Xml.CString _ since] -> since
   in PropDoc {
        propdoc_name = name,
        propdoc_paragraphs = concatMap extractDocPara paras,
        propdoc_since = since
      }

extractDocChildProp :: Xml.Content -> PropDoc
extractDocChildProp
  (Xml.CElem (Xml.Elem "childprop" []
    [Xml.CElem (Xml.Elem "name" [] [Xml.CString _ name])
    ,Xml.CElem (Xml.Elem "since" [] since')
    ,Xml.CElem (Xml.Elem "doc" [] paras)]
  )) =
  let since = case since' of
                [] -> ""
                [Xml.CString _ since] -> since
   in PropDoc {
        propdoc_name = name,
        propdoc_paragraphs = concatMap extractDocPara paras,
        propdoc_since = since
      }

extractDocSignal :: Xml.Content -> SignalDoc
extractDocSignal
  (Xml.CElem (Xml.Elem "signal" []
    [Xml.CElem (Xml.Elem "name" [] [Xml.CString _ name])
    ,Xml.CElem (Xml.Elem "since" [] since')
    ,Xml.CElem (Xml.Elem "doc" [] paras)
    ,Xml.CElem (Xml.Elem "params" [] params)]
  )) =
  let since = case since' of
                [] -> ""
                [Xml.CString _ since] -> since
   in SignalDoc {
        signaldoc_name = name,
        signaldoc_paragraphs = concatMap extractDocPara paras,
        signaldoc_params = map extractParamDoc params,
        signaldoc_since = since
      }

extractDocPara :: Xml.Content -> [DocPara]
extractDocPara (Xml.CElem elem@(Xml.Elem "para" [] _)) =
  case Xml.xmlUnEscape Xml.stdXmlEscaper elem of
    (Xml.Elem _ [] spans) -> extractDocPara' spans
extractDocPara (Xml.CElem (Xml.Elem "programlisting" _ content)) =
  let listing = concat [ str | (Xml.CString _ str) <- content ] in
  [DocParaProgram listing]
extractDocPara (Xml.CElem (Xml.Elem "example" _
                 (Xml.CElem (Xml.Elem "title" [] [Xml.CString _ title])
                 :content) )) =
  [DocParaTitle title] ++ concatMap extractDocPara content
extractDocPara other = error $ "extractDocPara: " ++ Xml.verbatim other

extractDocPara' :: [Xml.Content] -> [DocPara]
extractDocPara' = reconstructParas [] . map extractDocParaOrSpan
  where reconstructParas :: [DocParaSpan] -> [Either DocParaSpan [DocPara]] -> [DocPara]
        reconstructParas []    [] = []
        reconstructParas spans [] = [DocParaText (reverse spans)]
        reconstructParas spans (Left  span:rest) = reconstructParas (span:spans) rest
        reconstructParas []    (Right paras:rest) = paras ++ reconstructParas [] rest
        reconstructParas spans (Right paras:rest) = DocParaText (reverse spans)
                                                 : paras ++ reconstructParas [] rest

extractDocParaOrSpan :: Xml.Content -> Either DocParaSpan [DocPara]
extractDocParaOrSpan (Xml.CElem (Xml.Elem "listitem" [] content)) =
  Right [DocParaListItem (map extractDocParaSpan content)]
extractDocParaOrSpan (Xml.CElem (Xml.Elem "definition" []
                       (Xml.CElem (Xml.Elem "term" [] term)
                       :content))) =
  Right [DocParaDefItem (map extractDocParaSpan term) (map extractDocParaSpan content)]
extractDocParaOrSpan (Xml.CElem (Xml.Elem "programlisting" _ content)) =
  let listing = concat [ str | (Xml.CString _ str) <- content ] in
  Right [DocParaProgram listing]
extractDocParaOrSpan para@(Xml.CElem (Xml.Elem "para" _ _)) = Right (extractDocPara para)
extractDocParaOrSpan content@(Xml.CElem   _  ) = Left $ extractDocParaSpan content
extractDocParaOrSpan content@(Xml.CString _ _) = Left $ extractDocParaSpan content
extractDocParaOrSpan other = error $ "extractDocParaOrSpan: " ++ Xml.verbatim other

extractDocParaSpan :: Xml.Content -> DocParaSpan
extractDocParaSpan (Xml.CString _ text) = DocText text
extractDocParaSpan (Xml.CElem (Xml.Elem tag [] content)) =
  let text = concat [ str | (Xml.CString _ str) <- content ] in
  case tag of
    "xref-func"  -> DocFuncXRef text
    "xref-type"  -> DocTypeXRef text
    "xref-other" -> DocOtherXRef text
    "emphasis"   -> DocEmphasis text
    "literal"    -> DocLiteral text
    "arg"        -> DocArg text
    "section"    -> DocOtherXRef text
    _other -> error $ "extractDocParaSpan: other tag " ++ tag

extractDocParaSpan other@(Xml.CRef (Xml.RefEntity _entity)) = DocText (Xml.verbatim other)
extractDocParaSpan other = error $ "extractDocParaSpan: " ++ Xml.verbatim other


parseHierarchyDocs :: [DocParaSpan] -> Forest String
parseHierarchyDocs =
    forestFromPaths
  . map (reverse . map snd)
  . paths []
  . map extractLine
  . lines
  . extractLines

extractLines :: [DocParaSpan] -> String
extractLines = concatMap getText
  where getText (DocTypeXRef t) = t
        getText (DocText t)     = t

extractLine :: String -> (Int, String)
extractLine line =
  case span (==' ') line of
    (spaces, '+':'-':'-':'-':'-':remainder) -> (length spaces, remainder)
    (spaces, remainder)                     -> (length spaces, remainder)

paths :: [(Int,String)] -> [(Int,String)] -> [[(Int,String)]]
paths _ [] = []
paths ps ((col,name):remainder) = spec : paths spec remainder
  where
    parents = dropWhile (\(c,_) -> c>=col) ps
    spec = (col,name):parents

forestFromPaths :: Eq a => [[a]] -> Forest a
forestFromPaths = unfoldForest step . groupBy (equating head)
  where step paths = (label, children)
          where label = head (head paths)
                children = groupBy (equating head)
                             [ path | (_:path@(_:_)) <- paths ]

equating :: Eq a => (b -> a) -> b -> b -> Bool
equating p x y = p x == p y
