{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module    : Nix.JenkinsPlugins2Nix.Parser
-- Copyright : (c) 2017 Mateusz Kowalczyk
-- License   : BSD3
--
-- Parsers.
module Nix.JenkinsPlugins2Nix.Parser
  ( parseManifest
  , runParseManifest
  ) where

import           Control.Applicative
import           Control.Monad (void)
import qualified Data.Attoparsec.Text as A
import           Data.Either (either)
import           Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import           Data.Monoid ((<>))
import           Data.Set (Set)
import qualified Data.Set as Set
import           Data.Text (Text)
import qualified Data.Text as Text
import           Nix.JenkinsPlugins2Nix.Types

-- | Run parser on manifest file contents.
runParseManifest :: Text -> Either String Manifest
runParseManifest = A.parseOnly parseManifest

-- | 'Manifest' parser.
parseManifest :: A.Parser Manifest
parseManifest = do
  kvs <- kvMap
  let getKey :: Text -> Either String Text
      getKey k = case Map.lookup k kvs of
        Nothing -> Left $ "Could not find " <> Text.unpack k <> " in " <> show kvs
        Just v -> Right v

      getKeyParsing :: Text -> A.Parser a -> Either String a
      getKeyParsing k p = getKey k >>= A.parseOnly p

      eManifest :: Either String Manifest
      eManifest = do
        manifest_version' <- getKey "Manifest-Version"
        archiver_version' <- optional $ getKey "Archiver-Version"
        created_by' <- optional $ getKey "Created-By"
        built_by' <- optional $ getKey "Built-By"
        build_jdk' <- optional $ getKey "Build-Jdk"
        extension_name' <- optional $ getKey "Extension-Name"
        specification_title' <- optional $ getKey "Specification-Title"
        implementation_title' <- optional $ getKey "Implementation-Title"
        implementation_version' <- optional $ getKey "Implementation-Version"
        group_id' <- optional $ getKey "Group-Id"
        short_name' <- getKey "Short-Name"
        long_name' <- getKey "Long-Name"
        url' <- getKey "Url"
        plugin_version' <- getKey "Plugin-Version"
        hudson_version' <- optional $ getKey "Hudson-Version"
        jenkins_version' <- optional $ getKey "Jenkins-Version"
        plugin_dependencies' <- either (\_ -> Right Set.empty) return $
          getKeyParsing "Plugin-Dependencies" parsePluginDependencies
        plugin_developers' <- either (\_ -> Right Set.empty) return $
          getKeyParsing "Plugin-Developers" parsePluginDevelopers
        return $! Manifest
          { manifest_version = manifest_version'
          , archiver_version = archiver_version'
          , created_by = created_by'
          , built_by = built_by'
          , build_jdk = build_jdk'
          , extension_name = extension_name'
          , specification_title = specification_title'
          , implementation_title = implementation_title'
          , implementation_version = implementation_version'
          , group_id = group_id'
          , short_name = short_name'
          , long_name = long_name'
          , url = url'
          , plugin_version = plugin_version'
          , hudson_version = hudson_version'
          , jenkins_version = jenkins_version'
          , plugin_dependencies = plugin_dependencies'
          , plugin_developers = plugin_developers'
          }
  case eManifest of
    Left err -> fail err
    Right m -> return m
  where
    parsePluginDevelopers :: A.Parser (Set Text)
    parsePluginDevelopers = Set.fromList . Text.splitOn "," <$> A.takeText

    parsePluginDependencies :: A.Parser (Set PluginDependency)
    parsePluginDependencies =
      let plugin = do
            name <- A.takeWhile1 (/= ':') <* A.char ':'
            version <- A.takeWhile1 (\c -> c /= ',' && c /= ';')
            resolution <- A.peekChar >>= \case
              -- end of input
              Nothing -> return Mandatory
              -- next dependency
              Just ',' -> A.char ',' *> return Mandatory
              -- specifier
              Just ';' -> do
                _ <- A.string ";resolution:="
                A.takeWhile1 (/= ',') >>= \case
                  "optional" -> return Optional
                  res' -> fail $ "Don't know how to parse resolution: " <> Text.unpack res'
              Just c -> fail $ "plugin: expected , or ; but got: " <> [c]

            -- Consume trailing comma if any
            void (A.char ',') <|> return ()
            return $! PluginDependency
              { plugin_dependency_name = name
              , plugin_dependency_version = version
              , plugin_dependency_resolution = resolution
              }
      in Set.fromList <$> many plugin

    -- Lines in manifest can span multiple file lines as long as they
    -- are indented with a space on next physical line.
    kvEntry :: A.Parser (Text, Text)
    kvEntry = do
      key <- A.takeWhile1 (/= ':')
      A.anyChar *> A.skipSpace
      let restOfLine = A.takeTill A.isEndOfLine <* (A.endOfLine <|> return ())
          indentedLine = A.peekChar' >>= \case
            ' ' -> A.skipSpace *> restOfLine
            _ -> fail "indentedLine doesn't start with a space"

      valueLines <- (:) <$> restOfLine <*> many indentedLine
      return $! (key, Text.concat valueLines)

    kvMap :: A.Parser (Map Text Text)
    kvMap = Map.fromList <$> many kvEntry