{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RecordWildCards       #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE UndecidableInstances  #-}
{-# LANGUAGE ViewPatterns          #-}
module Language.Haskell.Homplexity.Comments (
    CommentLink      (..)
  , CommentType      (..)
  , classifyComments
  , findCommentType  -- exposed for testing only
  , CommentSite      (..)
  , commentable

  , orderCommentsAndCommentables
  ) where

import Data.Char
import Data.Data
import Data.Function
import Data.Functor
import Data.List
import qualified Data.Map.Strict as Map

import Language.Haskell.Homplexity.CodeFragment
import Language.Haskell.Homplexity.SrcSlice
import Language.Haskell.Exts.SrcLoc
import Language.Haskell.Exts

-- | Describes the comment span, and the way it may be connected to the
-- source code
data CommentLink = CommentLink { commentSpan :: SrcSpan
                               , commentType :: CommentType
                               }
  deriving(Eq, Ord, Show)

-- | Possible link between comment and commented entity.
data CommentType = CommentsBefore -- ^ May be counted as commenting object that starts just before.
                 | CommentsInside -- ^ May be counted as commenting object within which it exists.
                 | CommentsAfter  -- ^ May be counted as commenting object that starts just after.
  deriving (Eq, Ord, Enum, Show)

-- | Classifies all comments in list, so they can be assigned to declarations later.
classifyComments :: [Comment] -> [CommentLink]
classifyComments  = map classifyComment
  where
    classifyComment (Comment _ commentSpan (findCommentType -> commentType)) = CommentLink {..}

-- | Finds Haddock markers of which declarations the comment pertains to.
findCommentType :: String -> CommentType
findCommentType txt = case (not . isSpace) `find` txt of
  Just '^' -> CommentsBefore
  Just '|' -> CommentsAfter
  Just '*' -> CommentsInside -- since it comments the group of declarations, it belongs to the containing object
  _        -> CommentsInside

-- * Finding ranges of all commentable entities.
-- | Tagging of source range for each commentable object.
data CommentSite = CommentSite { siteName  :: String
                               , siteSlice :: SrcSlice
                               }
  deriving (Eq, Show)

newtype Ends   = End   { siteEnded   :: CommentSite }
  deriving (Eq, Show)

compareStarts :: CommentSite -> CommentSite -> Ordering
compareStarts = on compare (start . siteSlice)

instance Ord Ends   where
  compare = on compareEnds siteEnded

compareEnds :: CommentSite -> CommentSite -> Ordering
compareEnds  = on compare (end   . siteSlice)

start, end :: SrcSlice -> (Int, Int)
start slice = (srcSpanStartColumn slice, srcSpanStartLine slice)
end   slice = (srcSpanEndColumn   slice, srcSpanEndLine   slice)

-- | Find comment sites for entire program.
commentable     :: Data from => from -> [CommentSite]
commentable code = ($ code) `concatMap` [slicesOf functionT
                                        ,slicesOf typeSignatureT
                                        ,slicesOf moduleT       ]
  where
    commentSite  ::  CodeFragment c => (c -> SrcSlice) -> c -> CommentSite
    commentSite with frag = CommentSite (fragmentName frag)
                                        (with         frag)
    commentSites :: (CodeFragment c, Data from) => (c -> SrcSlice) -> Proxy c -> from -> [CommentSite]
    commentSites with fragType = map (commentSite with) . occursOf fragType
    slicesOf :: (CodeFragment c, Data from) => Proxy c -> from -> [CommentSite]
    slicesOf = commentSites              fragmentSlice
    --locsOf   = commentSites (locAsSpan . fragmentLoc)

-- | Take together are commentable elements, and all comments, and order them by source location.
orderCommentsAndCommentables :: [CommentSite] -> [CommentLink] -> [Either CommentLink CommentSite]
orderCommentsAndCommentables sites comments  = sortBy (compare `on` loc) elts
  where
    loc :: Either CommentLink CommentSite -> (SrcSpan, Bool)
    loc (Left  (commentSpan -> srcSpan)) = (srcSpan, True )
    loc (Right (siteSlice   -> srcSpan)) = (srcSpan, False)
    elts = (Left <$> comments) ++ (Right <$> sites)

type Assignment = Map.Map CommentSite [CommentLink]
{-
-- | Assign comments to the commentable elements.
assignComments :: [Either CommentLink CommentSite]
               -> [Assignment]
assignComments  = foldr assign ([], [], [], [])
  where
    assign :: (Assignment, [Assignment], [CommentLink]
    assign (assigned, unclosed, commentingAfter) nextElt = case nextElt of
      Left  (s@(CommentSite {}))                            ->
        (assigned, (s,commentingAfter):unclosed, [])
      Right (c@(CommentLink {commentType=CommentAfter,  ..}) -> 
        (assigned,                     unclosed, c:commentingAfter)
      Right (c@(CommentLink {commentType=CommentBefore, ..}) -> 
        (assigned,                     unclosed, c:commentingAfter)
      Right (c@(CommentLink {commentType=CommentInside, ..}) -> 
        (assigned,                     unclosed, c:commentingAfter)
 -}