/*
 * Copyright 2007-2008 WorldWide Conferencing, LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 */
package net.liftweb.http.js

import _root_.scala.xml.{NodeSeq, Group, Unparsed, Elem}
import _root_.net.liftweb.util.Helpers._
import _root_.net.liftweb.util.Helpers
import _root_.net.liftweb.util.TimeHelpers
import _root_.net.liftweb.util._
import _root_.scala.xml.{Node, SpecialNode, Text}

object JsCommands {
  def create = new JsCommands(Nil)
  def apply(in: Seq[JsCmd]) = new JsCommands(in.toList.reverse)
  def apply(in: JsExp) = new JsCommands(List(in.cmd))
}

class JsCommands(val reverseList: List[JsCmd]) {
  def &(in: JsCmd) = new JsCommands(in :: reverseList)
  def &(in: List[JsCmd]) = new JsCommands(in.reverse ::: reverseList)

  def toResponse = {
    val data = reverseList.reverse.map(_.toJsCmd).mkString("\n").getBytes("UTF-8")
    InMemoryResponse(data, List("Content-Length" -> data.length.toString, "Content-Type" -> "text/javascript"), Nil, 200)
  }
}

case class JsonCall(funcId: String) {
  def exp(exp: JsExp): JsCmd = JsCmds.Run(funcId+"("+exp.toJsCmd+");")

  def apply(command: String): JsCmd = apply(JE.Str(command))

  def apply(command: JsExp): JsCmd =
  JsCmds.Run(funcId+"({'command': "+command.toJsCmd+", 'params': false});")

  def apply(command: String, params: JsExp) =
  JsCmds.Run(funcId+"({'command': "+command.encJs+", 'params':"+
             params.toJsCmd+"});")

  def apply(command: String, target: String, params: JsExp) =
  JsCmds.Run(funcId+"({'command': "+command.encJs+", 'target': "+
             target.encJs+
             ", 'params':"+
             params.toJsCmd+"});")


  def apply(command: JsExp, params: JsExp) =
  JsCmds.Run(funcId+"({'command': "+command.toJsCmd+", 'params':"+
             params.toJsCmd+"});")

  def apply(command: JsExp, target: JsExp, params: JsExp) =
  JsCmds.Run(funcId+"({'command': "+command.toJsCmd+", 'target': "+
             target.toJsCmd+
             ", 'params':"+
             params.toJsCmd+"});")

}


trait JsObj extends JsExp {
  def props: List[(String,JsExp)]
  def toJsCmd = props.map{case (n, v) => n.encJs+": "+v.toJsCmd}.mkString("{", ", ", "}")
  def +*(other: JsObj) = {
    val np = props ::: other.props
    new JsObj {
      def props = np
    }
  }
}

trait JsExp extends SpecialNode with HtmlFixer with JxBase with ToJsCmd {
  def toJsCmd: String

  // def label: String = "#JS"

  override def toString(sb: StringBuilder) = {
    (new Text(toJsCmd)).toString(sb)
  }

  def appendToParent(parentName: String): JsCmd = {
    val ran = "v"+Helpers.nextFuncName
    JsCmds.JsCrVar(ran, this) &
    JE.JsRaw("if ("+ran+".parentNode) "+ran+" = "+ran+".cloneNode(true)").cmd &
    JE.JsRaw("if ("+ran+".nodeType) {"+parentName+".appendChild("+ran+");} else {"+
             parentName+".appendChild(document.createTextNode("+ran+"));}").cmd
  }

  /**
   * ~> accesses a property in the current JsExp
   */
  def ~>(right: JsMethod): JsExp =  new JsExp {
    def toJsCmd = JsExp.this.toJsCmd + "." + right.toJsCmd
  }

  def cmd: JsCmd = JsCmds.Run(toJsCmd+";")


  def +(right: JsExp): JsExp = new JsExp {
    def toJsCmd = JsExp.this.toJsCmd + " + "+ right.toJsCmd
  }

  def ===(right: JsExp): JsExp = new JsExp {
    def toJsCmd = JsExp.this.toJsCmd + " = "+ right.toJsCmd
  }

}

trait JsMethod {
  def toJsCmd: String
}

/**
 * JavaScript Expressions. To see these in action, check out
 * sites/example/src/webapp/json.html
 */
object JE {
  implicit def strToS(in: String): Str = Str(in)
  implicit def boolToJsExp(in: Boolean): JsExp = if (in) JsTrue else JsFalse
  implicit def numToJsExp(in: Int): JsExp = Num(in)
  implicit def numToJsExp(in: Long): JsExp = Num(in)
  implicit def numToJsExp(in: Double): JsExp = Num(in)
  implicit def numToJsExp(in: Float): JsExp = Num(in)

  case class Num(n: Number) extends JsExp {
    def toJsCmd = n.toString
  }

  case class Stringify(in: JsExp) extends JsExp {
    def toJsCmd = "JSON.stringify("+in.toJsCmd+")"
  }

  case class JsArray(in: JsExp*) extends JsExp {
    def toJsCmd = new JsExp {
      def toJsCmd = in.map(_.toJsCmd).mkString("[",", ", "]\n")
    }.toJsCmd

    def this(in: List[JsExp]) = this(in :_*)
  }

  case class ValById(id: String) extends JsExp {
    def toJsCmd = "document.getElementById("+id.encJs+").value"
  }

  /**
   * gets the element by ID
   */
  case class ElemById(id: String, then: String*) extends JsExp {
    override def toJsCmd = "document.getElementById("+id.encJs+")" + (
      if (then.isEmpty) "" else then.mkString(".", ".", "")
    )
  }

  object LjSwappable {
    def apply(visible: JsExp, hidden: JsExp): JxBase = {
      new JxNodeBase {
        def child = Nil
        def appendToParent(name: String): JsCmd =
        JsRaw(name+".appendChild(lift$.swappable("+visible.toJsCmd
              +", "+hidden.toJsCmd +"))").cmd
      }
    }

    def apply(visible: NodeSeq, hidden: NodeSeq): JxBase = {
      new JxNodeBase {
        def child = Nil
        def appendToParent(name: String): JsCmd =
        JsRaw(name+".appendChild(lift$.swappable("+AnonFunc(
            JsCmds.JsCrVar("df", JsRaw("document.createDocumentFragment()")) &
            addToDocFrag("df", visible.toList) &
            JE.JsRaw("return df").cmd
          ).toJsCmd
              +"(), "+AnonFunc(JsCmds.JsCrVar("df", JsRaw("document.createDocumentFragment()")) &
                               addToDocFrag("df", hidden.toList) &
                               JE.JsRaw("return df").cmd).toJsCmd +"()))").cmd
      }
    }
  }

  object LjBuildIndex {
    def apply(obj: String,
              indexName: String, tables: (String, String)*): JsExp = new JsExp {
      def toJsCmd = "lift$.buildIndex("+obj+", "+indexName.encJs+
      (if (tables.isEmpty) "" else ", "+
       tables.map{case (l, r) => "["+l.encJs+", "+r.encJs+"]"}.mkString(", "))+
      ")"
    }

    def apply(obj: JsExp,
              indexName: String, tables: (String, String)*): JsExp = new JsExp {
      def toJsCmd = "lift$.buildIndex("+obj.toJsCmd+", "+indexName.encJs+
      (if (tables.isEmpty) "" else ", "+
       tables.map{case (l, r) => "["+l.encJs+", "+r.encJs+"]"}.mkString(", "))+
      ")"
    }
  }

  protected trait MostLjFuncs {
    def funcName: String

    def apply(obj: String, func: String): JsExp = new JsExp {
      def toJsCmd = "lift$."+funcName+"("+obj+", "+func.encJs+")"
    }

    def apply(obj: JsExp, func: JsExp): JsExp = new JsExp {
      def toJsCmd = "lift$."+funcName+"("+obj.toJsCmd+", "+func.toJsCmd+")"
    }
  }

  object LjAlt {

    def apply(obj: String, func: String, alt: String): JsExp = new JsExp {
      def toJsCmd = "lift$.alt("+obj+", "+func.encJs+", "+alt.encJs+")"
    }

    def apply(obj: JsExp, func: JsExp, alt: String): JsExp = new JsExp {
      def toJsCmd = "lift$.alt("+obj.toJsCmd+", "+func.toJsCmd+", "+alt.encJs+")"
    }

    def apply(obj: JsExp, func: JsExp, alt: JsExp): JsExp = new JsExp {
      def toJsCmd = "lift$.alt("+obj.toJsCmd+", "+func.toJsCmd+", "+alt.toJsCmd+")"
    }
  }

  object LjMagicUpdate {
    def apply(obj: String, field: String, idField: String, toUpdate: JsExp): JsExp = new JsExp {
      def toJsCmd = "lift$.magicUpdate("+obj+", "+field.encJs+", "+idField.encJs+", "+toUpdate.toJsCmd+")"
    }

    def apply(obj: JsExp, field: String, idField: String, toUpdate: JsExp): JsExp = new JsExp {
      def toJsCmd = "lift$.magicUpdate("+obj.toJsCmd+", "+field.encJs+", "+idField.encJs+", "+toUpdate.toJsCmd+")"
    }
  }

  object LjForeach extends MostLjFuncs {
    def funcName: String = "foreach"
  }

  object LjFilter extends MostLjFuncs {
    def funcName: String = "filter"
  }

  object LjMap extends MostLjFuncs {
    def funcName: String = "map"
  }

  object LjFold {
    def apply(what: JsExp, init: JsExp, func: String): JsExp = new JsExp {
      def toJsCmd = "lift$.fold("+what.toJsCmd+", "+init.toJsCmd+", "+func.encJs+")"
    }

    def apply(what: JsExp, init: JsExp, func: AnonFunc): JsExp = new JsExp {
      def toJsCmd = "lift$.fold("+what.toJsCmd+", "+init.toJsCmd+", "+func.toJsCmd+")"
    }
  }

  object LjFlatMap extends MostLjFuncs {
    def funcName: String = "flatMap"
  }

  object LjSort extends MostLjFuncs {
    def funcName: String = "sort"

    def apply(obj: String): JsExp = new JsExp {
      def toJsCmd = "lift$."+funcName+"("+obj+")"
    }

    def apply(obj: JsExp): JsExp = new JsExp {
      def toJsCmd = "lift$."+funcName+"("+obj.toJsCmd+")"
    }
  }

  object FormToJSON {
    def apply(formId: String) =  new JsExp {
      def toJsCmd = LiftRules.jsArtifacts.formToJSON(formId).toJsCmd;
    }
  }

  /**
   * A String (JavaScript encoded)
   */
  case class Str(str: String) extends JsExp {
    def toJsCmd = str.encJs
  }

  /**
   * A JavaScript method that takes parameters
   */
  case class JsFunc(method: String, params: JsExp*) extends JsMethod {
    def toJsCmd = params.map(_.toJsCmd).mkString(method+"(", ", ", ")")
    def cmd: JsCmd = JsCmds.Run(toJsCmd+";")
  }

  /**
   * Put any JavaScript expression you want in here and the result will be
   * evaluated.
   */
  case class JsRaw(rawJsCmd: String) extends JsExp {
    def toJsCmd = rawJsCmd
  }

  case class JsVar(varName: String, andThen: String*) extends JsExp {
    def toJsCmd = varName + (if (andThen.isEmpty) ""
                             else andThen.mkString(".", ".", ""))
  }

  /**
   * A value that can be retrieved from an expression
   */
  case class JsVal(valueName: String) extends JsMethod {
    def toJsCmd = valueName
  }

  case object Id extends JsMethod {
    def toJsCmd = "id"
  }

  case object Style extends JsMethod {
    def toJsCmd = "style"
  }

  case object Value extends JsMethod {
    def toJsCmd = "value"
  }

  case object JsFalse extends JsExp {
    def toJsCmd = "false"
  }

  case object JsNull extends JsExp {
    def toJsCmd = "null"
  }

  case object JsTrue extends JsExp {
    def toJsCmd = "true"
  }

  case class Call(function: String, params: JsExp*) extends JsExp {
    def toJsCmd = function+"("+params.map(_.toJsCmd).mkString(",")+")"
  }

  trait AnonFunc extends JsExp {
    def applied: JsExp = new JsExp {
      def toJsCmd = "("+AnonFunc.this.toJsCmd + ")" + "()"
    }
    def applied(params: JsExp*): JsExp = new JsExp {
      def toJsCmd = "("+AnonFunc.this.toJsCmd +")" +
      params.map(_.toJsCmd).mkString("(", ",", ")")
    }

  }

  object AnonFunc {
    def apply(in: JsCmd): AnonFunc = new JsExp with AnonFunc {
      def toJsCmd = "function() {"+in.toJsCmd+"}"
    }

    def apply(params: String, in: JsCmd): AnonFunc = new JsExp with AnonFunc {
      def toJsCmd = "function("+params+") {"+in.toJsCmd+"}"
    }
  }

  object JsObj {
    def apply(members: (String, JsExp)*): JsObj =
    new JsObj {
      def props = members.toList
    }
  }

  case class JsLt(left: JsExp, right: JsExp) extends JsExp {
    def toJsCmd = left.toJsCmd + " < " + right.toJsCmd
  }

  case class JsGt(left: JsExp, right: JsExp) extends JsExp {
    def toJsCmd = left.toJsCmd + " > " + right.toJsCmd
  }

  case class JsEq(left: JsExp, right: JsExp) extends JsExp {
    def toJsCmd = left.toJsCmd + " == " + right.toJsCmd
  }

  case class JsNotEQ(left: JsExp, right: JsExp) extends JsExp {
    def toJsCmd = left.toJsCmd + " != " + right.toJsCmd
  }

  case class JsLtEq(left: JsExp, right: JsExp) extends JsExp {
    def toJsCmd = left.toJsCmd + " <= " + right.toJsCmd
  }

  case class JsGtEq(left: JsExp, right: JsExp) extends JsExp {
    def toJsCmd = left.toJsCmd + " >= " + right.toJsCmd
  }

}

trait HtmlFixer {
  def fixHtml(uid: String, content: NodeSeq): String =
  AltXML.toXML(Group(S.session.map(s => s.fixHtml(s.processSurroundAndInclude("JS SetHTML id: "+uid, content))).openOr(content)),
               false, true, S.ieMode).encJs

}

trait JsCmd extends HtmlFixer with ToJsCmd {
  def &(other: JsCmd): JsCmd = JsCmds.CmdPair(this, other)
  def toJsCmd: String
}

object JsCmds {
  implicit def seqJsToJs(in: Seq[JsCmd]): JsCmd = in.foldLeft[JsCmd](Noop)(_ & _)

  object Script {
    def apply(script: JsCmd): NodeSeq = <script type="text/javascript">{
        Unparsed("""
// <![CDATA[
"""+  script.toJsCmd+ """
// ]]>
""")
      }</script>
  }

  def JsHideId(what: String): JsCmd = LiftRules.jsArtifacts.hide(what).cmd

  def JsShowId(what: String): JsCmd = LiftRules.jsArtifacts.show(what).cmd

  case class SetHtml(uid: String, content: NodeSeq) extends JsCmd {
    def toJsCmd = LiftRules.jsArtifacts.setHtml(uid, content).toJsCmd
  }


  /**
   * Makes the parameter the selected HTML element on load of the page
   *
   * @param in the element that should have focus
   *
   * @return the element and a script that will give the element focus
   */
  object FocusOnLoad {
    def apply(in: Elem): NodeSeq = {
      val (elem, id) = findOrAddId(in)
      elem ++ Script(LiftRules.jsArtifacts.onLoad(Run("document.getElementById("+id.encJs+").focus();")))
    }
  }

  object Function {
    def apply(name: String, params: List[String], body: JsCmd): JsCmd =
    new JsCmd {
      def toJsCmd = "function "+name+"("+
      params.mkString(", ")+""") {
    """+body.toJsCmd+"""
    }
"""
    }
  }

  object OnLoad{
    def apply(what: JsCmd): JsCmd = LiftRules.jsArtifacts.onLoad(what)

  }

  case class SetValById(id: String, right: JsExp) extends JsCmd {
    def toJsCmd = "document.getElementById("+id.encJs+").value = "+
    right.toJsCmd+";"
  }

  case class SetExp(left: JsExp, right: JsExp) extends JsCmd {
    def toJsCmd = left.toJsCmd + " = " + right.toJsCmd + ";"
  }

  case class JsCrVar(name: String, right: JsExp) extends JsCmd {
    def toJsCmd = "var "+name + " = "+right.toJsCmd + ";"
  }

  case class SetElemById(id: String, right: JsExp, then: String*) extends JsCmd {
    def toJsCmd = "document.getElementById("+id.encJs+")"+ (
      if (then.isEmpty) "" else then.mkString(".", ".", "")
    ) + " = "+right.toJsCmd + ";"
  }

  implicit def jsExpToJsCmd(in: JsExp) = in.cmd

  case class CmdPair(left: JsCmd, right: JsCmd) extends JsCmd {
    def toJsCmd = {
      val sb = new StringBuilder
      append(sb, this)
      sb.toString
    }

    private def append(sb: StringBuilder, cmd: JsCmd) {
      cmd match {
        case CmdPair(l, r) => append(sb, l)
          sb.append('\n')
          append(sb, r)

        case c => sb.append(c.toJsCmd)
      }
    }
  }

  trait HasTime {
    def time: Box[TimeSpan]
    def timeStr = time.map(_.millis.toString) openOr ""
  }

  case class After(time: TimeSpan, toDo: JsCmd) extends JsCmd {
    def toJsCmd = "setTimeout(function() {"+toDo.toJsCmd+"}, "+time.millis+");"
  }

  case class Alert(text: String) extends JsCmd {
    def toJsCmd = "alert("+text.encJs+");"
  }

  case class Run(text: String) extends JsCmd {
    def toJsCmd = text
  }

  case object _Noop extends JsCmd {
    def toJsCmd = ""
  }

  implicit def cmdToString(in: JsCmd): String = in.toJsCmd

  val Noop: JsCmd = _Noop

  case class JsTry(what: JsCmd, alert: Boolean) extends JsCmd {
    def toJsCmd = "try { "+what.toJsCmd+" } catch (e) {"+(if (alert) "alert(e);" else "")+"}"
  }

  case class RedirectTo(where: String) extends JsCmd {
    private val context = S.contextPath
    def toJsCmd = "window.location = "+S.encodeURL(context + where).encJs+";"
  }


  /**
   * Update a Select with new Options
   */
  case class ReplaceOptions(select: String, opts: List[(String, String)], dflt: Box[String]) extends JsCmd {
    def toJsCmd = """var x=document.getElementById("""+select.encJs+""");
    while (x.length > 0) {x.remove(0);}
    var y = null;
    """+
    opts.map{case (value, text) =>
        "y=document.createElement('option'); "+
        "y.text = "+text.encJs+"; "+
        "y.value = "+value.encJs+"; "+
        (if (value == dflt) "y.selected = true; " else "") +
        " try {x.add(y, null);} catch(e) {if (typeof(e) == 'object' && typeof(e.number) == 'number' && (e.number & 0xFFFF) == 5){ x.add(y,x.options.length); } } "
    }.mkString("\n")
  }

  case object JsIf {
    def apply(condition: JsExp, body: JsCmd):JsCmd = JE.JsRaw("if ( " + condition.toJsCmd  + " ) { " + body.toJsCmd + " }")

    def apply(condition: JsExp, bodyTrue: JsCmd, bodyFalse: JsCmd) : JsCmd =
    JE.JsRaw("if ( " + condition.toJsCmd  +" ) { " + bodyTrue.toJsCmd + " } else { " + bodyFalse.toJsCmd + " }")

    def apply(condition: JsExp, body: JsExp):JsCmd = JE.JsRaw("if ( " + condition.toJsCmd  + " ) { " + body.toJsCmd + " }")

    def apply(condition: JsExp, bodyTrue: JsExp, bodyFalse: JsExp) : JsCmd =
    JE.JsRaw("if ( " + condition.toJsCmd  +" ) { " + bodyTrue.toJsCmd + " } else { " + bodyFalse.toJsCmd + " }")
  }

  case class JsWhile(condition: JsExp, body: JsExp) extends JsCmd {
    def toJsCmd = "while ( " + condition.toJsCmd + " ) { " + body.toJsCmd + " }"
  }

  case class JsWith(reference: String, body: JsExp) extends JsCmd {
    def toJsCmd = "with ( " + reference + " ) { " + body.toJsCmd + " }"
  }

  case class JsDoWhile(body: JsExp, condition: JsExp) extends JsCmd {
    def toJsCmd = "do { " + body.toJsCmd + " } while ( " + condition.toJsCmd + " )"
  }

  case class JsFor(initialExp: JsExp, condition: JsExp, incrementExp: JsExp, body: JsExp) extends JsCmd {
    def toJsCmd = "for ( " + initialExp.toJsCmd + "; " +
    condition.toJsCmd + "; " +
    incrementExp.toJsCmd + " ) { " + body.toJsCmd + " }"
  }

  case class JsForIn(initialExp: JsExp, reference: String, body: JsCmd) extends JsCmd {
    def toJsCmd = "for ( " + initialExp.toJsCmd + " in " + reference+ ") { " + body.toJsCmd + " }"
  }

  case object JsBreak extends JsCmd {
    def toJsCmd = "break"
  }

  case object JsContinue extends JsCmd {
    def toJsCmd = "continue"
  }

  object JsReturn {
    def apply(in: JsExp): JsCmd = new JsCmd {
      def toJsCmd = "return " + in.toJsCmd
    }
    def apply(): JsCmd =  new JsCmd {
      def toJsCmd = "return "
    }
  }

}