{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}

module Language.LSP.Protocol.Types.SemanticTokens where

import Data.Text (Text)

import Control.Monad.Except

import Language.LSP.Protocol.Internal.Types.SemanticTokenModifiers
import Language.LSP.Protocol.Internal.Types.SemanticTokenTypes
import Language.LSP.Protocol.Internal.Types.SemanticTokens
import Language.LSP.Protocol.Internal.Types.SemanticTokensDelta
import Language.LSP.Protocol.Internal.Types.SemanticTokensEdit
import Language.LSP.Protocol.Internal.Types.SemanticTokensLegend
import Language.LSP.Protocol.Types.Common
import Language.LSP.Protocol.Types.LspEnum

import Data.Algorithm.Diff qualified as Diff
import Data.Bits qualified as Bits
import Data.DList qualified as DList
import Data.Foldable hiding (
  length,
 )
import Data.Map qualified as Map
import Data.Maybe (
  fromMaybe,
  maybeToList,
 )
import Data.String

defaultSemanticTokensLegend :: SemanticTokensLegend
defaultSemanticTokensLegend =
  SemanticTokensLegend
    (fmap toEnumBaseType . toList $ knownValues @SemanticTokenTypes)
    (fmap toEnumBaseType . toList $ knownValues @SemanticTokenModifiers)

----------------------------------------------------------
-- Tools for working with semantic tokens.
----------------------------------------------------------

{- | A single 'semantic token' as described in the LSP specification, using absolute positions.
 This is the kind of token that is usually easiest for editors to produce.
-}
data SemanticTokenAbsolute = SemanticTokenAbsolute
  { _line :: UInt
  , _startChar :: UInt
  , _length :: UInt
  , _tokenType :: SemanticTokenTypes
  , _tokenModifiers :: [SemanticTokenModifiers]
  }
  deriving stock (Show, Eq, Ord)

-- Note: we want the Ord instance to sort the tokens textually: this is achieved due to the
-- order of the constructors

-- | A single 'semantic token' as described in the LSP specification, using relative positions.
data SemanticTokenRelative = SemanticTokenRelative
  { _deltaLine :: UInt
  , _deltaStartChar :: UInt
  , _length :: UInt
  , _tokenType :: SemanticTokenTypes
  , _tokenModifiers :: [SemanticTokenModifiers]
  }
  deriving stock (Show, Eq, Ord)

-- Note: we want the Ord instance to sort the tokens textually: this is achieved due to the
-- order of the constructors

{- | Turn a list of absolutely-positioned tokens into a list of relatively-positioned tokens. The tokens are assumed to be in the
 order that they appear in the document!
-}
relativizeTokens :: [SemanticTokenAbsolute] -> [SemanticTokenRelative]
relativizeTokens xs = DList.toList $ go 0 0 xs mempty
 where
  -- Pass an accumulator to make this tail-recursive
  go :: UInt -> UInt -> [SemanticTokenAbsolute] -> DList.DList SemanticTokenRelative -> DList.DList SemanticTokenRelative
  go _ _ [] acc = acc
  go lastLine lastChar (SemanticTokenAbsolute l c len ty mods : ts) acc =
    let
      lastCharInLine = if l == lastLine then lastChar else 0
      dl = l - lastLine
      dc = c - lastCharInLine
     in
      go l c ts (DList.snoc acc (SemanticTokenRelative dl dc len ty mods))

{- | Turn a list of relatively-positioned tokens into a list of absolutely-positioned tokens. The tokens are assumed to be in the
 order that they appear in the document!
-}
absolutizeTokens :: [SemanticTokenRelative] -> [SemanticTokenAbsolute]
absolutizeTokens xs = DList.toList $ go 0 0 xs mempty
 where
  -- Pass an accumulator to make this tail-recursive
  go :: UInt -> UInt -> [SemanticTokenRelative] -> DList.DList SemanticTokenAbsolute -> DList.DList SemanticTokenAbsolute
  go _ _ [] acc = acc
  go lastLine lastChar (SemanticTokenRelative dl dc len ty mods : ts) acc =
    let
      lastCharInLine = if dl == 0 then lastChar else 0
      l = lastLine + dl
      c = lastCharInLine + dc
     in
      go l c ts (DList.snoc acc (SemanticTokenAbsolute l c len ty mods))

-- | Encode a series of relatively-positioned semantic tokens into an integer array following the given legend.
encodeTokens :: SemanticTokensLegend -> [SemanticTokenRelative] -> Either Text [UInt]
encodeTokens SemanticTokensLegend{_tokenTypes = tts, _tokenModifiers = tms} sts =
  DList.toList . DList.concat <$> traverse encodeToken sts
 where
  -- Note that there's no "fast" version of these (e.g. backed by an IntMap or similar)
  -- in general, due to the possibility  of unknown token types which are only identified by strings.
  tyMap :: Map.Map SemanticTokenTypes UInt
  tyMap = Map.fromList $ zip (fmap fromOpenEnumBaseType tts) [0 ..]
  modMap :: Map.Map SemanticTokenModifiers Int
  modMap = Map.fromList $ zip (fmap fromOpenEnumBaseType tms) [0 ..]

  lookupTy :: SemanticTokenTypes -> Either Text UInt
  lookupTy ty = case Map.lookup ty tyMap of
    Just tycode -> pure tycode
    Nothing -> throwError $ "Semantic token type " <> fromString (show ty) <> " did not appear in the legend"
  lookupMod :: SemanticTokenModifiers -> Either Text Int
  lookupMod modifier = case Map.lookup modifier modMap of
    Just modcode -> pure modcode
    Nothing -> throwError $ "Semantic token modifier " <> fromString (show modifier) <> " did not appear in the legend"

  -- Use a DList here for better efficiency when concatenating all these together
  encodeToken :: SemanticTokenRelative -> Either Text (DList.DList UInt)
  encodeToken (SemanticTokenRelative dl dc len ty mods) = do
    tycode <- lookupTy ty
    modcodes <- traverse lookupMod mods
    let combinedModcode :: Int = foldl' Bits.setBit Bits.zeroBits modcodes

    pure [dl, dc, len, tycode, fromIntegral combinedModcode]

-- This is basically 'SemanticTokensEdit', but slightly easier to work with.

-- | An edit to a buffer of items.
data Edit a = Edit {editStart :: UInt, editDeleteCount :: UInt, editInsertions :: [a]}
  deriving stock (Read, Show, Eq, Ord)

-- | Compute a list of edits that will turn the first list into the second list.
computeEdits :: Eq a => [a] -> [a] -> [Edit a]
computeEdits l r = DList.toList $ go 0 Nothing (Diff.getGroupedDiff l r) mempty
 where
  {-
  Strategy: traverse the list of diffs, keeping the current index and (maybe) an in-progress 'Edit'.
  Whenever we see a 'Diff' that's only one side or the other, we can bundle that in to our in-progress
  'Edit'. We only have to stop if we see a 'Diff' that's on both sides (i.e. unchanged), then we
  dump the 'Edit' into the accumulator.
  We need the index, because 'Edit's need to say where they start.
  -}
  go :: UInt -> Maybe (Edit a) -> [Diff.Diff [a]] -> DList.DList (Edit a) -> DList.DList (Edit a)
  -- No more diffs: append the current edit if there is one and return
  go _ e [] acc = acc <> DList.fromList (maybeToList e)
  -- Items only on the left (i.e. deletions): increment the current index, and record the count of deletions,
  -- starting a new edit if necessary.
  go ix e (Diff.First ds : rest) acc =
    let
      deleteCount = fromIntegral $ Prelude.length ds
      edit = fromMaybe (Edit ix 0 []) e
     in
      go (ix + deleteCount) (Just (edit{editDeleteCount = editDeleteCount edit + deleteCount})) rest acc
  -- Items only on the right (i.e. insertions): don't increment the current index, and record the insertions,
  -- starting a new edit if necessary.
  go ix e (Diff.Second as : rest) acc =
    let edit = fromMaybe (Edit ix 0 []) e
     in go ix (Just (edit{editInsertions = editInsertions edit <> as})) rest acc
  -- Items on both sides: increment the current index appropriately (since the items appear on the left),
  -- and append the current edit (if there is one) to our list of edits (since we can't continue it with a break).
  go ix e (Diff.Both bs _bs : rest) acc =
    let bothCount = fromIntegral $ Prelude.length bs
     in go (ix + bothCount) Nothing rest (acc <> DList.fromList (maybeToList e))

-- | Convenience method for making a 'SemanticTokens' from a list of 'SemanticTokenAbsolute's. An error may be returned if

-- The resulting 'SemanticTokens' lacks a result ID, which must be set separately if you are using that.
makeSemanticTokens :: SemanticTokensLegend -> [SemanticTokenAbsolute] -> Either Text SemanticTokens
makeSemanticTokens legend sts = do
  encoded <- encodeTokens legend $ relativizeTokens sts
  pure $ SemanticTokens Nothing encoded

{- | Convenience function for making a 'SemanticTokensDelta' from a previous and current 'SemanticTokens'.
 The resulting 'SemanticTokensDelta' lacks a result ID, which must be set separately if you are using that.
-}
makeSemanticTokensDelta :: SemanticTokens -> SemanticTokens -> SemanticTokensDelta
makeSemanticTokensDelta SemanticTokens{_data_ = prevTokens} SemanticTokens{_data_ = curTokens} =
  let edits = computeEdits prevTokens curTokens
      stEdits = fmap (\(Edit s ds as) -> SemanticTokensEdit s ds (Just as)) edits
   in SemanticTokensDelta Nothing stEdits
