module Generate.JavaScript (generate) where

import Control.Monad.State as State
import qualified Data.List as List
import Language.ECMAScript3.PrettyPrint as ES
import Language.ECMAScript3.Syntax
import qualified Text.PrettyPrint.Leijen as PP

import qualified AST.Expression.Optimized as Optimized
import qualified AST.Helpers as Help
import qualified AST.Module as Module
import qualified AST.Module.Name as ModuleName
import qualified AST.Variable as Variable
import qualified Generate.JavaScript.Expression as JS
import Generate.JavaScript.Helpers (call, function, localRuntime, obj, prop, ref, varDecl)
import qualified Generate.JavaScript.Variable as Var


generate :: Module.Optimized -> String
generate module_ =
  let
    (ModuleName.Canonical _ name) =
      Module.name module_

    body =
      concat
        [ useStrict
        , checkForCachedVersion name
        , imports (Module.imports module_)
        , definitions (Module.program (Module.body module_))
        , exports name (Module.exports module_)
        ]

    make =
      "Elm" : name ++ ["make"]

    javascript =
      buildIntermediateObjects make
      ++
      [ assign make (function [localRuntime] body)
      ]
  in
    PP.displayS (PP.renderPretty 1 160 (ES.prettyPrint javascript)) ""



-- INTRO


useStrict :: [Statement ()]
useStrict =
  [ ExprStmt () (StringLit () "use strict")
  ]


checkForCachedVersion :: ModuleName.Raw -> [Statement ()]
checkForCachedVersion name =
  let
    values =
      localRuntime : name ++ ["values"]

    valuesObject =
      obj values
  in
    buildIntermediateObjects values
    ++
    [ IfSingleStmt () valuesObject (ReturnStmt () (Just valuesObject))
    ]


buildIntermediateObjects :: [String] -> [Statement ()]
buildIntermediateObjects path =
  let
    create name =
      assign name (InfixExpr () OpLOr (obj name) (ObjectLit () []))

    paths =
      drop 2 (init (List.inits path))
  in
    map create paths


assign :: [String] -> Expression () -> Statement ()
assign path expr =
  case path of
    [x] ->
        VarDeclStmt () [ varDecl x expr ]

    _ ->
        ExprStmt () $
            AssignExpr () OpAssign (LDot () (obj (init path)) (last path)) expr



-- IMPORTS


imports :: [ModuleName.Raw] -> [Statement ()]
imports rawImports =
  let
    utils =
      varDecl "_U" (call (obj ["Elm", "Native", "Utils", "make"]) [ref localRuntime])
  in
    [ VarDeclStmt () (utils : map jsImport rawImports) ]


jsImport :: ModuleName.Raw -> VarDecl ()
jsImport name =
  let
    make =
      obj ("Elm" : name ++ ["make"])
  in
    varDecl (Var.modulePrefix name) (call make [ref localRuntime])



-- DEFINITIONS


definitions :: [Optimized.Def] -> [Statement ()]
definitions defs =
  let
    setup =
      Var.define "_op" (ObjectLit () [])
  in
    setup : State.evalState (mapM JS.generateDef defs) 0



-- EXPORTS


exports :: ModuleName.Raw -> [Variable.Value] -> [Statement ()]
exports name exportedValues =
  let
    exportedFields =
      map toField ("_op" : concatMap extract exportedValues)

    toField x =
      (prop x, ref x)

    extract value =
        case value of
          Variable.Alias _ ->
              []

          Variable.Value x ->
              if Help.isOp x then
                  []

              else
                  [Var.safe x]

          Variable.Union _ (Variable.Listing ctors _) ->
              map Var.safe ctors

    values =
      LDot () (obj (localRuntime : name)) "values"
  in
    [ ReturnStmt () $ Just $
        AssignExpr () OpAssign values (ObjectLit () exportedFields)
    ]
