{-# LANGUAGE CPP                   #-}
{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GADTs                 #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE ViewPatterns          #-}

module Ide.Plugin.Cabal.Outline where

import           Control.Monad.IO.Class
import           Data.Maybe
import qualified Data.Text                               as T
import           Data.Text.Encoding                      (decodeUtf8)
import           Development.IDE.Core.Rules
import           Development.IDE.Core.Shake              (IdeState (shakeExtras),
                                                          runIdeAction,
                                                          useWithStaleFast)
import           Development.IDE.Types.Location          (toNormalizedFilePath')
import           Distribution.Fields.Field               (Field (Field, Section),
                                                          Name (Name))
import           Distribution.Parsec.Position            (Position)
import           Ide.Plugin.Cabal.Completion.CabalFields (onelineSectionArgs)
import           Ide.Plugin.Cabal.Completion.Types       (ParseCabalFields (..),
                                                          cabalPositionToLSPPosition)
import           Ide.Plugin.Cabal.Orphans                ()
import           Ide.Types                               (PluginMethodHandler)
import           Language.LSP.Protocol.Message           (Method (..))
import           Language.LSP.Protocol.Types             (DocumentSymbol (..))
import qualified Language.LSP.Protocol.Types             as LSP


moduleOutline :: PluginMethodHandler IdeState Method_TextDocumentDocumentSymbol
moduleOutline ideState _ LSP.DocumentSymbolParams {_textDocument = LSP.TextDocumentIdentifier uri} =
  case LSP.uriToFilePath uri of
    Just (toNormalizedFilePath' -> fp) -> do
      mFields <- liftIO $ runIdeAction "cabal-plugin.fields" (shakeExtras ideState) (useWithStaleFast ParseCabalFields fp)
      case fmap fst mFields of
        Just fieldPositions -> pure $ LSP.InR (LSP.InL allSymbols)
          where
            allSymbols = mapMaybe documentSymbolForField fieldPositions
        Nothing -> pure $ LSP.InL []
    Nothing -> pure $ LSP.InL []

-- | Creates a @DocumentSymbol@ object for the
--   cabal AST, without displaying @fieldLines@ and
--   displaying @Section Name@ and @SectionArgs@ in one line.
--
--   @fieldLines@ are leaves of a cabal AST, so they are omitted
--   in the outline. Sections have to be displayed in one line, because
--   the AST representation looks unnatural. See examples:
--
-- *  part of a cabal file:
--
-- >   if impl(ghc >= 9.8)
-- >      ghc-options: -Wall
--
-- * AST representation:
--
-- >   if
-- >      impl
-- >      (
-- >      ghc >= 9.8
-- >      )
-- >
-- >      ghc-options:
-- >        -Wall
--
-- * resulting @DocumentSymbol@:
--
-- >   if impl(ghc >= 9.8)
-- >      ghc-options:
-- >
documentSymbolForField :: Field Position -> Maybe DocumentSymbol
documentSymbolForField (Field (Name pos fieldName) _) =
  Just
    (defDocumentSymbol range)
      { _name = decodeUtf8 fieldName,
        _kind = LSP.SymbolKind_Field,
        _children = Nothing
      }
  where
    range = cabalPositionToLSPRange pos `addNameLengthToLSPRange` decodeUtf8 fieldName
documentSymbolForField (Section (Name pos fieldName) sectionArgs fields) =
  Just
    (defDocumentSymbol range)
      { _name = joinedName,
        _kind = LSP.SymbolKind_Object,
        _children =
          Just
            (mapMaybe documentSymbolForField fields)
      }
  where
    joinedName = decodeUtf8 fieldName <> " " <> onelineSectionArgs sectionArgs
    range = cabalPositionToLSPRange pos `addNameLengthToLSPRange` joinedName

-- | Creates a single point LSP range
--   using cabal position
cabalPositionToLSPRange :: Position -> LSP.Range
cabalPositionToLSPRange pos = LSP.Range lspPos lspPos
  where
    lspPos = cabalPositionToLSPPosition pos

addNameLengthToLSPRange :: LSP.Range -> T.Text -> LSP.Range
addNameLengthToLSPRange (LSP.Range pos1 (LSP.Position line char)) name =
  LSP.Range
    pos1
    (LSP.Position line (char + fromIntegral (T.length name)))

defDocumentSymbol :: LSP.Range -> DocumentSymbol
defDocumentSymbol range = DocumentSymbol
  { _detail = Nothing
  , _deprecated = Nothing
  , _name = ""
  , _kind = LSP.SymbolKind_File
  , _range = range
  , _selectionRange = range
  , _children = Nothing
  , _tags = Nothing
  }
