content: module VimScript where
  
  import Data.Char (isAlpha, isAlphaNum, isSpace, ord, toLower)
  import Data.Default (def)
  import Data.List (foldl', nub, sort)
  import Data.List.Extra (groupSort)
  import Data.Map.Strict qualified as M
  import Data.Text (Text, singleton, unpack, unwords)
  import Data.Text qualified as T
  import Data.Text.Builder.Linear qualified as Builder
  import Data.Text.Display (Display(..), display)
  import Prelude hiding (unwords)
  
  import Cmdline
  import Command
  import Mapping
  import Mode
  import Plugin
  import Setting
  
  newtype VimScript = VimScript (M.Map Place [Text])
                    deriving Eq
  
  data Place = Plugin'
             | Autoload Text
             | Ftplugin Text
             | Syntax   Text
             deriving (Eq, Ord)
  
  instance Display Place where
    displayBuilder = Builder.fromText . \case
      Plugin'     -> "plugin/miv.vim"
      Autoload "" -> "autoload/miv.vim"
      Autoload s  -> "autoload/miv/" <> s <> ".vim"
      Ftplugin s  -> "ftplugin/" <> s <> ".vim"
      Syntax s    -> "syntax/" <> s <> ".vim"
  
  vimScriptToList :: VimScript -> [(Place, [Text])]
  vimScriptToList (VimScript x) = M.toList x
  
  instance Semigroup VimScript where
    VimScript x <> VimScript y
      | M.null x = VimScript y
      | M.null y = VimScript x
      | otherwise = VimScript (M.unionWith concat' x y)
      where
        concat' a [] = a
        concat' [] b = b
        concat' a b  = a <> [""] <> b
  
  instance Monoid VimScript where
    mempty = VimScript M.empty
  
  gatherScript :: Setting -> VimScript
  gatherScript setting = beforeScript setting
                      <> gatherBeforeAfterScript setting.plugins
                      <> gather' "dependon" (.dependon) (.loadbefore) setting.plugins
                      <> gather' "dependedby" (.dependedby) (.loadafter) setting.plugins
                      <> gather "mappings" (.mappings) setting.plugins
                      <> gather "mapmodes" (map display . (.mapmodes)) setting.plugins
                      <> gather "functions" (.functions) setting.plugins
                      <> pluginLoader
                      <> mappingLoader
                      <> commandLoader
                      <> foldl' (<>) mempty (map pluginConfig setting.plugins)
                      <> filetypeLoader setting
                      <> funcUndefinedLoader setting
                      <> cmdlineEnterLoader setting
                      <> insertEnterLoader setting
                      <> filetypeScript setting
                      <> syntaxScript setting
                      <> afterScript setting
  
  gatherBeforeAfterScript :: [Plugin] -> VimScript
  gatherBeforeAfterScript x = insertAuNameMap $ gatherScripts x (mempty, M.empty)
    where
      insertAuNameMap :: (VimScript, M.Map Text Text) -> VimScript
      insertAuNameMap (vs, m) = VimScript (M.singleton (Autoload "") $
            [ "let s:autoload = {" ]
         <> [ "      \\ " <> singleQuote k <> ": " <> singleQuote a <> "," | (k, a) <- M.toList m ]
         <> [ "      \\ }" ]) <> vs
      gatherScripts :: [Plugin] -> (VimScript, M.Map Text Text) -> (VimScript, M.Map Text Text)
      gatherScripts (p:ps) (vs, m)
              | null p.before && null p.after = gatherScripts ps (vs, m)
              | otherwise = gatherScripts ps (vs <> vs', M.insert name hchar m)
        where
          name = T.filter isAlphaNum (T.toLower (display p))
          hchar | null (loadScript p) = maybe "_" singleton $ getHeadChar $ display p
                | otherwise = "_"
          funcname str = "miv#" <> hchar <> "#" <> str <> "_" <> name
          vs' = VimScript $ M.singleton (Autoload hchar) $
            wrapFunction (funcname "before") p.before <>
            wrapFunction (funcname "after") p.after
      gatherScripts [] (vs, m) = (vs, m)
  
  gather :: Text -> (Plugin -> [Text]) -> [Plugin] -> VimScript
  gather name f plugin
    = VimScript (M.singleton (Autoload "") $
        [ "let s:" <> name <> " = {" ]
     <> [ "      \\ " <> singleQuote (display p)
                      <> ": [ " <> T.intercalate ", " (map singleQuote (f p))  <> " ],"
          | p <- plugin, p.enable /= "0", not (null (f p)) ]
     <> [ "      \\ }" ])
  
  gather' :: Text -> (Plugin -> [Text]) -> (Plugin -> [Text]) -> [Plugin] -> VimScript
  gather' name f g plugin
    = VimScript (M.singleton (Autoload "") $
        [ "let s:" <> name <> " = {" ]
     <> [ "      \\ " <> singleQuote p
                      <> ": [ " <> T.intercalate ", " (map singleQuote $ sort $ nub q)  <> " ],"
          | (p, q) <- groupSort $ [ (display p, q) | p <- plugin, p.enable /= "0", q <- f p ]
                               <> [ (q, display p) | p <- plugin, p.enable /= "0", q <- g p ] ]
     <> [ "      \\ }" ])
  
  pluginConfig :: Plugin -> VimScript
  pluginConfig plugin
    = VimScript (M.singleton Plugin' $ wrapInfo $
        wrapEnable plugin.enable $ mapleader <> gatherCommand plugin <> gatherMapping plugin <> plugin.script <> loadScript plugin)
    where
      wrapInfo []  = []
      wrapInfo str = ("\" " <> plugin.name) : str
      mapleader = let s = plugin.mapleader in ["let g:mapleader = " <> singleQuote s | not (T.null s)]
  
  loadScript :: Plugin -> [Text]
  loadScript plugin
    | all null [ plugin.commands, plugin.mappings, plugin.functions, plugin.filetypes
               , plugin.loadafter, plugin.loadbefore ] && null plugin.cmdlines && not plugin.insert
    = ["call miv#load(" <> singleQuote (display plugin) <> ")"]
    | otherwise = []
  
  gatherCommand :: Plugin -> [Text]
  gatherCommand plugin
    | not (null plugin.commands)
      = [display def
            { Command.name = c,
              Command.repl = unwords [
                "call miv#command(" <> singleQuote (display plugin) <> ",",
                singleQuote c <> ",",
                singleQuote "<bang>" <> ",",
                "<q-args>,",
                "expand('<line1>'),",
                "expand('<line2>'))"
            ] } | c <- plugin.commands]
    | otherwise = []
  
  gatherMapping :: Plugin -> [Text]
  gatherMapping plugin
    | not (null plugin.mappings)
      = [display def
            { Mapping.name = mapping,
              Mapping.repl = escape mode <> ":<C-u>call miv#mapping("
                  <> singleQuote (display plugin) <> ", "
                  <> singleQuote mapping <> ", "
                  <> singleQuote (display mode) <> ")<CR>",
              Mapping.mode = mode
            } | mapping <- plugin.mappings, mode <- modes]
    | otherwise = []
      where modes = if null plugin.mapmodes then [NormalMode] else plugin.mapmodes
            escape mode = if mode `elem` [InsertMode, OperatorPendingMode] then "<ESC>" else ""
  
  beforeScript :: Setting -> VimScript
  beforeScript setting = VimScript (M.singleton Plugin' setting.before)
  
  afterScript :: Setting -> VimScript
  afterScript setting = VimScript (M.singleton Plugin' setting.after)
  
  filetypeLoader :: Setting -> VimScript
  filetypeLoader setting =
    mconcat $ map (uncurry f) $ groupSort [(ft, p) | p <- setting.plugins, ft <- p.filetypes]
    where
      f :: Text -> [Plugin] -> VimScript
      f ft plugins = flip foldMap (getHeadChar ft) $
        \c -> let funcname = "miv#" <> singleton c <> "#load_" <> T.filter isAlphaNum (T.toLower ft)
                  in VimScript (M.singleton Plugin'
                       [ "augroup miv-file-type-" <> ft
                       , "  autocmd!"
                       , "  autocmd FileType " <> ft <> " call " <> funcname <> "()"
                       , "augroup END" ]) <>
                     VimScript (M.singleton (Autoload (singleton c)) $
                       ("function! " <> funcname <> "() abort")
                       : "  setlocal filetype="
                       : loadPlugins plugins
                    <> [ "  autocmd! miv-file-type-" <> ft
                       , "  augroup! miv-file-type-" <> ft
                       , "  setlocal filetype=" <> ft
                       , "  silent! doautocmd FileType " <> ft
                       , "endfunction" ])
  
  wrapEnable :: Text -> [Text] -> [Text]
  wrapEnable _ [] = []
  wrapEnable "" strs = strs
  wrapEnable "0" _ = []
  wrapEnable enable strs@(s:_) =
    [indent <> "if " <> enable] <>
          map ("  "<>) strs <>
    [indent <> "endif"]
    where indent = T.takeWhile isSpace s
  
  loadPlugins :: [Plugin] -> [Text]
  loadPlugins plugins = concat
    [wrapEnable enable
      ["  call miv#load(" <> singleQuote (display p) <> ")" | p <- plugins']
        | (enable, plugins') <- groupSort [(p.enable, p) | p <- plugins]]
  
  singleQuote :: Text -> Text
  singleQuote str = "'" <> str <> "'"
  
  filetypeScript :: Setting -> VimScript
  filetypeScript setting = foldMap (\(ft, src) -> VimScript (M.singleton (Ftplugin ft) src)) $ M.toList setting.filetype
  
  syntaxScript :: Setting -> VimScript
  syntaxScript setting = foldMap (\(ft, src) -> VimScript (M.singleton (Syntax ft) src)) $ M.toList setting.syntax
  
  wrapFunction :: Text -> [Text] -> [Text]
  wrapFunction funcname script =
       ["function! " <> funcname <> "() abort"]
    <> map ("  "<>) script
    <> ["endfunction"]
  
  getHeadChar :: Text -> Maybe Char
  getHeadChar xs
    | T.null xs = Nothing
    | otherwise = let x = T.head xs in if isAlpha x then Just (toLower x) else getHeadChar (T.tail xs)
  
  mappingLoader :: VimScript
  mappingLoader = VimScript (M.singleton (Autoload "")
    [ "function! miv#mapping(name, mapping, mode) abort"
    , "  call miv#load(a:name)"
    , "  if a:mode ==# 'v' || a:mode ==# 'x'"
    , "    call feedkeys('gv', 'n')"
    , "  elseif a:mode ==# 'o'"
    , "    call feedkeys(\"\\<Esc>\", 'n')"
    , "    call feedkeys(v:operator, 'm')"
    , "  endif"
    , "  call feedkeys((v:count ? v:count : '') . substitute(a:mapping, '<Plug>', \"\\<Plug>\", 'g'), 'm')"
    , "  return ''"
    , "endfunction" ])
  
  commandLoader :: VimScript
  commandLoader = VimScript (M.singleton (Autoload "")
    [ "function! miv#command(name, command, bang, args, line1, line2) abort"
    , "  silent! execute 'delcommand' a:command"
    , "  call miv#load(a:name)"
    , "  let range = a:line1 != a:line2 ? a:line1.','.a:line2 : ''"
    , "  try"
    , "    exec range.a:command.a:bang a:args"
    , "  catch /^Vim\\%((\\a\\+)\\)\\=:E481:/"
    , "    exec a:command.a:bang a:args"
    , "  endtry"
    , "endfunction" ])
  
  funcUndefinedLoader :: Setting -> VimScript
  funcUndefinedLoader setting = if null functions then mempty else
    VimScript (M.singleton Plugin'
    [ "\" FuncUndefined"
    , "augroup miv-func-undefined"
    , "  autocmd!"
    , "  autocmd FuncUndefined * call miv#func_undefined(expand('<amatch>'))"
    , "augroup END" ])
    <> VimScript (M.singleton (Autoload "")
    [ "function! miv#func_undefined(pattern) abort"
    , "  if a:pattern !~# " <> singleQuote (T.intercalate "\\|" functions)
    , "    return"
    , "  endif"
    , "  if empty(s:functions)"
    , "    autocmd! miv-func-undefined"
    , "    augroup! miv-func-undefined"
    , "    return"
    , "  endif"
    , "  for [name, fs] in items(s:functions)"
    , "    for f in fs"
    , "      if a:pattern =~# f"
    , "        call miv#load(name)"
    , "        return"
    , "      endif"
    , "    endfor"
    , "  endfor"
    , "endfunction" ])
    where functions = [f | p <- setting.plugins, f <- p.functions]
  
  cmdlineEnterLoader :: Setting -> VimScript
  cmdlineEnterLoader setting =
    mconcat $ map (uncurry f) $ groupSort [(cmdline, p) | p <- setting.plugins, cmdline <- p.cmdlines]
    where
      f :: Cmdline -> [Plugin] -> VimScript
      f cmdline plugins = VimScript (M.singleton Plugin'
        [ "\" CmdlineEnter " <> display cmdline
        , "if exists('#CmdlineEnter')"
        , "  augroup " <> group
        , "    autocmd!"
        , "    autocmd CmdlineEnter " <> cmdlinePattern cmdline <> " call miv#cmdline_enter_" <> c <> "()"
        , "  augroup END"
        , "else"
        , "  call miv#cmdline_enter_" <> c <> "()"
        , "endif" ])
        <> VimScript (M.singleton (Autoload "") $
         ("function! miv#cmdline_enter_" <> c <> "() abort")
        : loadPlugins plugins
        <> [ "  if exists('#CmdlineEnter')"
        , "    autocmd! " <> group
        , "    augroup! " <> group
        , "  endif"
        , "endfunction" ])
        where c = T.concat $ map (display . ord) (unpack (display cmdline))
              group = "miv-cmdline-enter-" <> c
  
  insertEnterLoader :: Setting -> VimScript
  insertEnterLoader setting = if null plugins then mempty else
    VimScript (M.singleton Plugin'
    [ "\" InsertEnter"
    , "augroup miv-insert-enter"
    , "  autocmd!"
    , "  autocmd InsertEnter * call miv#insert_enter()"
    , "augroup END" ])
    <> VimScript (M.singleton (Autoload "")
    $ "function! miv#insert_enter() abort"
    : loadPlugins plugins
    <> [ "  autocmd! miv-insert-enter"
    , "  augroup! miv-insert-enter"
    , "  silent! doautocmd InsertEnter"
    , "endfunction" ])
    where plugins = filter (.insert) setting.plugins
  
  pluginLoader :: VimScript
  pluginLoader = VimScript (M.singleton (Autoload "")
    [ "let s:loaded = {}"
    , "let s:path = expand('<sfile>:p:h:h')"
    , "let s:mivpath = expand('<sfile>:p:h:h:h') . '/'"
    , "let s:rtpidx = 0"
    , "function! miv#load(name) abort"
    , "  if has_key(s:loaded, a:name)"
    , "    return"
    , "  endif"
    , "  let s:loaded[a:name] = 1"
    , "  if has_key(s:functions, a:name)"
    , "    call remove(s:functions, a:name)"
    , "  endif"
    , "  for n in get(s:dependon, a:name, [])"
    , "    call miv#load(n)"
    , "  endfor"
    , "  for mode in get(s:mapmodes, a:name, ['n'])"
    , "    for mapping in get(s:mappings, a:name, [])"
    , "      silent! execute mode . 'unmap' mapping"
    , "    endfor"
    , "  endfor"
    , "  let name = substitute(tolower(a:name), '[^a-z0-9]', '', 'g')"
    , "  let au = has_key(s:autoload, name)"
    , "  if au"
    , "    call miv#{s:autoload[name]}#before_{name}()"
    , "  endif"
    , "  let newrtp = s:mivpath . a:name"
    , "  if !isdirectory(newrtp)"
    , "    return"
    , "  endif"
    , "  let rtps = split(&rtp, ',')"
    , "  let s:rtpidx = max([index(rtps, s:path, s:rtpidx), 0])"
    , "  let &rtp = join(insert(rtps, newrtp, s:rtpidx), ',')"
    , "  if isdirectory(newrtp . '/after')"
    , "    exec 'set rtp+=' . newrtp . '/after'"
    , "  endif"
    , "  for dir in filter(['/plugin', '/after/plugin'], 'isdirectory(newrtp . v:val)')"
    , "    for file in split(glob(newrtp . dir . '/**/*.vim'), '\\n')"
    , "      silent! source `=file`"
    , "    endfor"
    , "  endfor"
    , "  if au"
    , "    call miv#{s:autoload[name]}#after_{name}()"
    , "  endif"
    , "  for n in get(s:dependedby, a:name, [])"
    , "    call miv#load(n)"
    , "  endfor"
    , "endfunction" ])
  