module Agda.Compiler.JS.Pretty where

import Prelude hiding ( null )
import Data.List ( intercalate )
import Data.Set ( Set, toList, singleton, insert, member )
import Data.Map ( Map, toAscList, empty, null )
import Text.Regex.TDFA (makeRegex, matchTest, Regex)

import Agda.Syntax.Common ( Nat )
import Agda.Utils.Hash

import Agda.Compiler.JS.Syntax hiding (exports)

-- Pretty-print a lambda-calculus expression as ECMAScript.

-- Since ECMAScript is C-like rather than Haskell-like, it's easier to
-- do the pretty-printing directly than use the Pretty library, which
-- assumes Haskell-like indentation.

br :: Int -> String
br i = "\n" ++ take (2*i) (repeat ' ')

unescape :: Char -> String
unescape '"'      = "\\\""
unescape '\\'     = "\\\\"
unescape '\n'     = "\\n"
unescape '\r'     = "\\r"
unescape '\x2028' = "\\u2028"
unescape '\x2029' = "\\u2029"
unescape c        = [c]

unescapes :: String -> String
unescapes s = concat (map unescape s)

-- pretty n i e pretty-prints e, under n levels of de Bruijn binding,
-- with i levels of indentation.

class Pretty a where
    pretty :: Nat -> Int -> a -> String

instance (Pretty a, Pretty b) => Pretty (a,b) where
  pretty n i (x,y) = pretty n i x ++ ": " ++ pretty n (i+1) y

-- Pretty-print collections

class Pretties a where
    pretties :: Nat -> Int -> a -> [String]

instance Pretty a => Pretties [a] where
  pretties n i = map (pretty n i)

instance (Pretty a, Pretty b) => Pretties (Map a b) where
  pretties n i o = pretties n i (toAscList o)

-- Pretty print identifiers

instance Pretty LocalId where
  pretty n i (LocalId x) = "x" ++ show (n - x - 1)

instance Pretty GlobalId where
  pretty n i (GlobalId m) = variableName $ intercalate "_" m

instance Pretty MemberId where
  pretty n i (MemberId s) = "\"" ++ unescapes s ++ "\""

-- Pretty print expressions

instance Pretty Exp where
  pretty n i (Self)                 = "exports"
  pretty n i (Local x)              = pretty n i x
  pretty n i (Global m)             = pretty n i m
  pretty n i (Undefined)            = "undefined"
  pretty n i (String s)             = "\"" ++ unescapes s ++ "\""
  pretty n i (Char c)               = "\"" ++ unescape c ++ "\""
  pretty n i (Integer x)            = "agdaRTS.primIntegerFromString(\"" ++ show x ++ "\")"
  pretty n i (Double x)             = show x
  pretty n i (Lambda x e)           =
    "function (" ++
      intercalate ", " (pretties (n+x) i (map LocalId [x-1, x-2 .. 0])) ++
    ") " ++ block (n+x) i e
  pretty n i (Object o) | null o    = "{}"
  pretty n i (Object o) | otherwise =
    "{" ++ br (i+1) ++ intercalate ("," ++ br (i+1)) (pretties n i o) ++ br i ++ "}"
  pretty n i (Apply f es)           = pretty n i f ++ "(" ++ intercalate ", " (pretties n i es) ++ ")"
  pretty n i (Lookup e l)           = pretty n i e ++ "[" ++ pretty n i l ++ "]"
  pretty n i (If e f g)             =
    "(" ++ pretty n i e ++ "? " ++ pretty n i f ++ ": " ++ pretty n i g ++ ")"
  pretty n i (PreOp op e)           = "(" ++ op ++ " " ++ pretty n i e ++ ")"
  pretty n i (BinOp e op f)         = "(" ++ pretty n i e ++ " " ++ op ++ " " ++ pretty n i f ++ ")"
  pretty n i (Const c)              = c
  pretty n i (PlainJS js)           = "(" ++ js ++ ")"

block :: Nat -> Int -> Exp -> String
block n i (If e f g) = "{" ++ br (i+1) ++ block' n (i+1) (If e f g) ++ br i ++ "}"
block n i e          = "{" ++ br (i+1) ++ "return " ++ pretty n (i+1) e ++ ";" ++ br i ++ "}"

block' :: Nat -> Int -> Exp -> String
block' n i (If e f g) = "if (" ++ pretty n i e ++ ") " ++ block n i f ++ " else " ++ block' n i g
block' n i e          = block n i e

modname :: GlobalId -> String
modname (GlobalId ms) = "\"" ++ intercalate "." ms ++ "\""


exports :: Nat -> Int -> Set [MemberId] -> [Export] -> String
exports n i lss [] = ""
exports n i lss (Export ls e : es) | member (init ls) lss =
  "exports[" ++ intercalate "][" (pretties n i ls) ++ "] = " ++ pretty n (i+1) e ++ ";" ++ br i ++
  exports n i (insert ls lss) es
exports n i lss (Export ls e : es) | otherwise =
  exports n i lss (Export (init ls) (Object empty) : Export ls e : es)

instance Pretty Module where
  pretty n i (Module m es ex) =
    imports ++ br i
      ++ exports n i (singleton []) es ++ br i
      ++ maybe "" (pretty n i) ex
    where
      js = toList (globals es)
      imports = unlines $
            ["var agdaRTS = require(\"agda-rts\");"] ++
            ["var " ++ pretty n (i+1) e ++ " = require(" ++ modname e ++ ");"
            | e <- js]

variableName :: String -> String
variableName s = if isValidJSIdent s then "z_" ++ s else "h_" ++ show (hashString s)

-- | Check if a string is a valid JS identifier. The check ignores keywords
-- as we prepend z_ to our identifiers. The check
-- is conservative and may not admit all valid JS identifiers.
isValidJSIdent :: String -> Bool
isValidJSIdent s = matchTest regex s
  where
    regex :: Regex
    regex = makeRegex ("^[a-zA-Z_$][0-9a-zA-Z_$]*$" :: String)
