-- Copyright 2014 Alvaro J. Genial [http://alva.ro]; see LICENSE file for more.

module Text.Neat.Input.Django (input) where

import Control.Applicative ((<$), (<$>), (<*), (<*>), (*>), (<|>), many, some)
import Data.Char (isAlphaNum, isSpace)
import Data.List (intercalate, span)
import Text.Parsec hiding ((<|>), many, optional)
import Text.Neat.Input
import Text.Neat.Template


input :: String -> String -> File
input path string = case runParser (file path) () path string of
    Right result -> result
    Left failure -> error $ "parsing failure at " ++ show failure


outputMarkers  = ("{{", "}}")
commentMarkers = ("{#", "#}")
elementMarkers = ("{%", "%}")


file :: String -> Parsec String () File
file path = File path <$> block <* eof where
  block = Block <$> many chunk
  chunk = Chunk <$> location <*> element
  element = choice $ try <$> [
    Output  <$> value' `within` outputMarkers,
    Comment <$> block  `within` commentMarkers,
    Define  <$> tag "def"    function <*> block              <* end "enddef",
    Filter  <$> tag "filter" value    <*> block              <* end "endfilter",
    For     <$> tag "for"    binding  <*> block <*> else'    <* end "endfor",
    If      <$> tag "if"     value    <*> block <*> else'    <* end "endif",
    Switch  <$> tag "switch" value    <*> cases <*> default' <* end "endswitch",
    With    <$> tag "with"   binding  <*> block              <* end "endwith",
    Text    <$> some textChar]

  value'   = value <$> location <*> trimmedText
  else'    = optionMaybe . try $ end "else" *> block
  default' = optionMaybe . try $ end "default" *> block
  cases    = spaces *> many (try case') where
    case'  = Case <$> tag "case" Pattern <*> block

  tag name f = let t = keyword name *> trimmedText
                in f <$> location <*> (t `within` elementMarkers)
  end name = keyword name `within` elementMarkers
  keyword k = string k <* notFollowedBy nameChar <* spaces

  p `within` (left, right) =
    between (string left <* spaces) (spaces *> string right) p

  trimmedText = spaces *> (trim <$> many textChar)
  trim = reverse . dropWhile isSpace . reverse . dropWhile isSpace

  value l s    = Value l (trim <$> split "|" s)
  function l s = Function l n (Pattern l p) where (n, p) = span isName s
  binding  l s = case split " in " s of
    [p, v] -> Binding (Pattern l (trim p)) (value l v)
    _      -> case split " as " s of
      [v, p] -> Binding (Pattern l (trim p)) (value l v)
      _      -> case split "=" s of
        [p, v] -> Binding (Pattern l (trim p)) (value l v)
        _      -> error $ "invalid for: " ++ s

  isName c = isAlphaNum c || c `elem` "'_"
  nameChar = alphaNum <|> oneOf "'_"
  textChar = noneOf "{#%}" -- TODO: Generate this from markers.
          <|> try (char '{' <* notFollowedBy (oneOf "{#%"))
          <|> try (oneOf "#%}" <* notFollowedBy (char '}'))
  location = f <$> getPosition
    where f p = Location (sourceName p) (sourceLine p)