{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE FlexibleContexts #-}

-- TODO: Remove as soon as the pretty printing stuff is actually used.
{-# OPTIONS_GHC -fno-warn-unused-binds #-}

-- | Basic low-level types and their combinators.
--   These are used as output of the compiler.
--   Everything here is untypes and not supposed for public use!
module Language.Sunroof.JavaScript
  ( Expr, ExprE(..), E(..)
  , Id, Stmt(..), Type(..)
  , Rhs(..)
  , showExpr, showStmt
  , operator, binOp, uniOp
  , literal
  , scopeForEffect
  ) where

import Data.List ( intercalate )
import Data.Reify ( MuRef(..) )
import Control.Applicative ( Applicative, pure, (<$>), (<*>))
import Data.Traversable ( Traversable(..) )
import Data.Foldable ( Foldable(..) )
import Data.Monoid ( Monoid(..) )
import Data.Char ( isAlpha, isAlphaNum )

-- -------------------------------------------------------------
-- Javascript Expressions
-- -------------------------------------------------------------

-- | Javascript identifier.
type Id = String

-- | Short name for instantiated expressions.
type Expr = E ExprE

-- | Instantiated expressions.
data ExprE = ExprE Expr deriving Show

-- | Plain expressions in Javascript.
data E expr = Lit String -- ^ A precompiled (atomic) Javascript literal.
            | Var Id     -- ^ A variable.
            | Dot expr expr Type   -- ^ Field/attribute access (with type information): @expr . expr :: Type@
            | Apply expr [expr]    -- ^ Function application: @expr ( expr, ..., expr )@
            | Function [Id] [Stmt] -- ^ Anonymous function with parameter names and body.
            deriving Show

instance MuRef ExprE where
  type DeRef ExprE = E
  mapDeRef f (ExprE e) = traverse f e

instance Traversable E where
  traverse _ (Lit s) = pure (Lit s)
  traverse _ (Var s) = pure (Var s)
  traverse f (Dot o n a) = Dot <$> f o <*> f n <*> pure a
  traverse f (Apply s xs) = Apply <$> f s <*> traverse f xs
  traverse _ (Function nms stmts) = pure (Function nms stmts)

instance Foldable E where
  foldMap _ (Lit _) = mempty
  foldMap _ (Var _) = mempty
  foldMap f (Dot o n _) = f o `mappend` f n
  foldMap f (Apply o xs) = f o `mappend` foldMap f xs
  foldMap _ (Function _nms _stmts) = mempty

instance Functor E where
  fmap _ (Lit s) = Lit s
  fmap _ (Var s) = Var s
  fmap f (Dot o n a) = Dot (f o) (f n) a
  fmap f (Apply s xs) = Apply (f s) (map f xs)
  fmap _ (Function nms stmts) = Function nms stmts

-- | Show an expression as compiled Javascript.
--   The boolean argument says non-trivial arguments need parenthesis.
showExpr :: Bool -> Expr -> String
-- Original comment: These being up here, cause a GHC warning for missing patterns.
--                   So they are moved down.
-- Response: They *need* to be here, it makes a different. I've fixed the warning.
showExpr _ (Lit a) = a  -- always stand alone, or pre-parenthesised
showExpr _ (Var v) = v  -- always stand alone
showExpr b e = p $ case e of
--    (Apply (ExprE (Var "[]")) [ExprE a,ExprE x])   -> showExpr True a ++ "[" ++ showExpr False x ++ "]"
    (Apply (ExprE (Var "?:")) [ExprE a,ExprE x,ExprE y]) -> showExpr True a ++ "?" ++ showExpr True x ++ ":" ++ showExpr True y
    (Apply (ExprE (Var op)) [ExprE x,ExprE y]) | not (any isAlpha op) -> showExpr True x ++ op ++ showExpr True y
    (Apply (ExprE (Var "!")) [ExprE ex]) -> "!" ++ showExpr True ex
    -- We have a constructor call:
    (Apply (ExprE (Lit op)) args) | isNewConstructor op -> op ++ showArgs args
    (Apply (ExprE fn) args) -> showFun fn args
    (Dot (ExprE a) (ExprE x) Base) -> showIdx a x
    -- This is a shortcomming in Javascript, where grabbing a indirected function
    -- throws away the context (self/this). So we force storage of the context, using a closure.
    (Dot (ExprE a) (ExprE x) (Fun xs _)) ->
        "function(" ++ intercalate "," args ++ ") { return (" ++
          showIdx a x ++ ")(" ++ intercalate "," args ++ "); }"
      where args = [ "a" ++ show i | i <- take (length xs) ([0..] :: [Int])]
    -- This pattern was missing too.
    (Dot (ExprE _a) (ExprE _x) Unit) ->
      error "Dot pattern on unit type. Don't know what to do."
    (Function args body) ->
      "function" ++
      "(" ++ intercalate "," args ++ ") {\n" ++
         indent 2 (unlines (map showStmt body)) ++
      "}"
    _ -> error "should never happen"
  where
    p txt = if b then "(" ++ txt ++ ")" else txt

-- | @showIdx o a@ accesses the field/attribute @a@ of the object @o@.
showIdx :: Expr -> Expr -> String
showIdx a (Lit x) | Just n <- isGoodSelectName x
                  = showExpr True a ++ "." ++ n
showIdx a ix = showExpr True a ++ "[" ++ showExpr False ix ++ "]"

-- | @showArgs a@ creates a string representing the given expressions
--   in an argument list that can be used for functions or constructors.
showArgs :: [ExprE] -> String
showArgs args = "(" ++ intercalate "," (map (\ (ExprE e') -> showExpr False e') args) ++ ")"

-- | Show a function application,
showFun :: Expr -> [ExprE] -> String
showFun e args = case e of
    (Dot (ExprE a) (ExprE (Lit x)) _)
        | Just n <- isGoodSelectName x -> showExpr True a ++ "." ++ n ++ showArgs args
    (Dot (ExprE a) (ExprE x) _) -> "(" ++ showIdx a x ++ ")" ++ showArgs args
    _                           -> showExpr True e ++ showArgs args

-- | Check if the given 'Id' is a valid Javascript identifier.
isIdentifier :: Id -> Bool
isIdentifier x | not (null x) = isAlpha (head x) && all isAlphaNum (drop 1 x)
isIdentifier _ = False

-- | Check if the given 'Id' represents a constructor call without
--   arguments. That means a string beginning with @"new "@ followed
--   by a valid identifier ('isIdentifier').
isNewConstructor :: Id -> Bool
isNewConstructor x = take 4 x == "new " && isIdentifier (drop 4 x)

-- | Check if the given name is a field/attribute seletor that
--   can be printed without quotes using the dot-notation.
isGoodSelectName :: Id -> Maybe Id
isGoodSelectName xs
        | length xs < 2 = Nothing
        | head xs == '"' &&
          last xs == '"' &&
          all isAlpha xs' = return xs'
        | otherwise = Nothing
  where
          xs' = tail (init xs)

-- -------------------------------------------------------------
-- Helper Combinators
-- -------------------------------------------------------------

-- | Combinator to create a operator/function applied to the given arguments.
operator :: Id -> [Expr] -> Expr
operator n ps = Apply (ExprE $ Var n) (fmap ExprE ps)

-- | Short-hand to create the applied binary operator/function.
--   See 'operator'.
binOp :: String -> Expr -> Expr -> E ExprE
binOp o e1 e2 = operator o [e1, e2]

-- | Short-hand to create the applied unary operator/function.
--   See 'operator'.
uniOp :: String -> Expr -> E ExprE
uniOp o e = operator o [e]

-- | Combinator to create a expression containing a
--   literal in form of a string.
literal :: String -> Expr
literal = Lit

-- | Indent all lines of the given string by the given number
--   of spaces.
indent :: Int -> String -> String
indent n = unlines . map (take n (cycle "  ") ++) . lines

-- | Create a anonymous function to scope all effects
--   in the given block of statement.
scopeForEffect :: [Stmt] -> Expr
scopeForEffect stmts = Apply (ExprE $ Function [] stmts) []

-- -------------------------------------------------------------
-- Javascript References
-- -------------------------------------------------------------

-- | A Right hand side of an assignment.

data Rhs = VarRhs Id                  -- ^ A variable
         | DotRhs Expr Expr           -- ^ a named field

showRhs :: Rhs -> String
showRhs (VarRhs var)   = "var " ++ var
showRhs (DotRhs e1 e2) = showIdx e1 e2

-- -------------------------------------------------------------
-- Javascript Statements
-- -------------------------------------------------------------

-- TODO: remove VarStmt, replace with AssignStmt and ExprStmt.
-- TODO: add type to return stmt; should not return "null"

-- | Plain Javascript statements.
data Stmt = AssignStmt Rhs Expr       -- ^ Restricted assignment: @Rhs = Expr;@
          | DeleteStmt Expr           -- ^ Delete reference @delete Rhs;@
          | ExprStmt Expr             -- ^ Expression statement, for the sake of its side effects: @Expr;@
          | ReturnStmt Expr           -- ^ Return statement: @return Expr;@
          | IfStmt Expr [Stmt] [Stmt] -- ^ If-Then-Else statement: @if (Expr) { Stmts } else { Stmts }@
          | WhileStmt Expr [Stmt]     -- ^ While loop: @while (Expr) { Stmts }@
          | CommentStmt String        -- ^ A comment in the code: @// String@

instance Show Stmt where
  show = showStmt

-- | Translate a statement into actual Javascript.
showStmt :: Stmt -> String
showStmt (AssignStmt e1 e2) = showRhs e1 ++ " = " ++ showExpr False e2 ++ ";"
showStmt (DeleteStmt e) = "delete " ++ showExpr False e ++ ";"
showStmt (ExprStmt e) = showExpr False e ++ ";"
showStmt (ReturnStmt e) = "return " ++ showExpr False e ++ ";"
showStmt (IfStmt i t e) = "if(" ++ showExpr False i ++ "){\n"
  ++ indent 2 (unlines (map showStmt t))
  ++ "} else {\n"
  ++ indent 2 (unlines (map showStmt e))
  ++ "}"
showStmt (WhileStmt b stmts) = "while(" ++ showExpr False b ++ "){\n"
  ++ indent 2 (unlines (map showStmt stmts))
  ++ "}"
showStmt (CommentStmt msg) = "/* " ++ msg ++ " */"

-- -------------------------------------------------------------
-- Javascript Types
-- -------------------------------------------------------------

-- | Abstract types for Javascript expressions in Sunroof.
data Type = Base -- ^ Base type like object or other primtive types.
          | Unit -- ^ Unit or void type. There is a effect but no value.
          | Fun [Type] Type -- ^ Function type: @(t_1,..,t_n) -> t@
          deriving (Eq,Ord)

instance Show Type where
  show Base    = "*"
  show Unit    = "()"
  show (Fun xs t) = show xs ++ " -> " ++ show t

-- -------------------------------------------------------------
-- Pretty Printer
-- -------------------------------------------------------------

data Doc = Text String           -- plain text (assume no newlines)
         | Indent Int Doc        -- indent document by n
         | Sep [Doc]             -- on seperate lines

text :: String -> Doc
text = Text

--indent :: Int -> Doc -> Doc
--indent = Indent

sep :: [Doc] -> Doc
sep = Sep

pretty :: Doc -> String
pretty (Text txt) = txt
pretty (Sep docs) = unlines $ map pretty docs
pretty (Indent n doc) = unlines $ map (take n (cycle "  ") ++) $ lines $ pretty doc