{-# LANGUAGE RecordWildCards #-}

module Language.Haskell.Stylish.Step.Data where

import           Data.List                       (find, intercalate)
import           Data.Maybe                      (fromMaybe, maybeToList)
import qualified Language.Haskell.Exts           as H
import           Language.Haskell.Exts.Comments
import           Language.Haskell.Stylish.Block
import           Language.Haskell.Stylish.Editor
import           Language.Haskell.Stylish.Step
import           Language.Haskell.Stylish.Util
import           Prelude                         hiding (init)

data Indent
    = SameLine
    | Indent !Int
  deriving (Show)

data Config = Config
    { cEquals           :: !Indent
      -- ^ Indent between type constructor and @=@ sign (measured from column 0)
    , cFirstField       :: !Indent
      -- ^ Indent between data constructor and @{@ line (measured from column with data constructor name)
    , cFieldComment     :: !Int
      -- ^ Indent between column with @{@ and start of field line comment (this line has @cFieldComment = 2@)
    , cDeriving         :: !Int
      -- ^ Indent before @deriving@ lines (measured from column 0)
    } deriving (Show)

datas :: H.Module l -> [H.Decl l]
datas (H.Module _ _ _ _ decls) = decls
datas _                        = []

type ChangeLine = Change String

step :: Config -> Step
step cfg = makeStep "Data" (step' cfg)

step' :: Config -> Lines -> Module -> Lines
step' cfg ls (module', allComments) = applyChanges changes ls
  where
    datas' = datas $ fmap linesFromSrcSpan module'
    changes = datas' >>= maybeToList . changeDecl allComments cfg

findCommentOnLine :: LineBlock -> [Comment] -> Maybe Comment
findCommentOnLine lb = find commentOnLine
  where
    commentOnLine (Comment _ (H.SrcSpan _ start _ end _) _) =
      blockStart lb == start && blockEnd lb == end

findCommentBelowLine :: LineBlock -> [Comment] -> Maybe Comment
findCommentBelowLine lb = find commentOnLine
  where
    commentOnLine (Comment _ (H.SrcSpan _ start _ end _) _) =
      blockStart lb == start - 1 && blockEnd lb == end - 1

commentsWithin :: LineBlock -> [Comment] -> [Comment]
commentsWithin lb = filter within
  where
    within (Comment _ (H.SrcSpan _ start _ end _) _) =
      start >= blockStart lb && end <= blockEnd lb

changeDecl :: [Comment] -> Config -> H.Decl LineBlock -> Maybe ChangeLine
changeDecl _ _ (H.DataDecl _ (H.DataType _) Nothing _ [] _) = Nothing
changeDecl allComments cfg@Config{..} (H.DataDecl block (H.DataType _) Nothing dhead decls derivings)
  | hasRecordFields = Just $ change block (const $ concat newLines)
  | otherwise       = Nothing
  where
    hasRecordFields = any
      (\qual -> case qual of
                  (H.QualConDecl _ _ _ (H.RecDecl {})) -> True
                  _ -> False)
      decls

    typeConstructor = "data " <> H.prettyPrint dhead

    -- In any case set @pipeIndent@ such that @|@ is aligned with @=@.
    (firstLine, firstLineInit, pipeIndent) =
      case cEquals of
        SameLine -> (Nothing, typeConstructor <> " = ", length typeConstructor + 1)
        Indent n -> (Just [[typeConstructor]], indent n "= ", n)

    newLines = fromMaybe [] firstLine ++ fmap constructors zipped <> [fmap (indent cDeriving . H.prettyPrint) derivings]
    zipped = zip decls ([1..] ::[Int])

    constructors (decl, 1) = processConstructor allComments firstLineInit cfg decl
    constructors (decl, _) = processConstructor allComments (indent pipeIndent "| ") cfg decl
changeDecl _ _ _ = Nothing

processConstructor :: [Comment] -> String -> Config -> H.QualConDecl LineBlock -> [String]
processConstructor allComments init Config{..} (H.QualConDecl _ _ _ (H.RecDecl _ dname (f:fs))) = do
  fromMaybe [] firstLine <> n1 <> ns <> [indent fieldIndent "}"]
  where
    n1 = processName firstLinePrefix (extractField f)
    ns = fs >>= processName (indent fieldIndent ", ") . extractField

    -- Set @fieldIndent@ such that @,@ is aligned with @{@.
    (firstLine, firstLinePrefix, fieldIndent) =
      case cFirstField of
        SameLine ->
          ( Nothing
          , init <> H.prettyPrint dname <> " { "
          , length init + length (H.prettyPrint dname) + 1
          )
        Indent n ->
          ( Just [init <> H.prettyPrint dname]
          , indent (length init + n) "{ "
          , length init + n
          )

    processName prefix (fnames, _type, lineComment, commentBelowLine) =
      [prefix <> intercalate ", " (fmap H.prettyPrint fnames) <> " :: " <> H.prettyPrint _type <> addLineComment lineComment
      ] ++ addCommentBelow commentBelowLine

    addLineComment (Just (Comment _ _ c)) = " --" <> c
    addLineComment Nothing                = ""

    -- Field comment indent is measured from the column with @{@, hence adding of @fieldIndent@ here.
    addCommentBelow Nothing                = []
    addCommentBelow (Just (Comment _ _ c)) = [indent (fieldIndent + cFieldComment) "--" <> c]

    extractField (H.FieldDecl lb names _type) =
      (names, _type, findCommentOnLine lb allComments, findCommentBelowLine lb allComments)

processConstructor _ init _ decl = [init <> trimLeft (H.prettyPrint decl)]
