package ulsp

import json
global triggerCharLoc

class SignatureHandler(
   filehandler,
   params,
   currentSigItems
   )

   method setVariables(new_filehandler, new_params)
      filehandler := new_filehandler
      params := new_params
   end

   method getResult()
      local line, character, methodName, results_table, desired_line, context, listPos, sig_table, c, every_other_sig_table

      # Collect line and character position #
      line := params["position"]["line"] + 1
      character := params["position"]["character"]

      # Create initial return table #
      results_table := table()
      results_table["signatures"] := []
      results_table["activeSignature"] := &null

     # Collect context information (inside comment, space before, object before, no object before) #
      context := filehandler.getContext(line, character) | fail
      desired_line := context.line
      \desired_line | return results_table
      \params["context"] | return results_table

      # Handle an active signature #
      if \params["context"]["activeSignatureHelp"] then {
         handleActiveSignature(params, results_table, context)
         return results_table
      }

      # If any signatures still active #
      handleActiveSignature("reset")

      # Handle a new signature #
      handleNewSignature(context, results_table)

      # Remove duplicate signatures #
      listPos := 1
      every sig_table := !results_table["signatures"] do {
         c := 0
         every every_other_sig_table := !results_table["signatures"] do {
            sig_table["label"] ? {
               if match(every_other_sig_table["label"]) then {
                  c +:= 1
               }
            }
         }
         if c > 1 then delete(results_table["signatures"], listPos)
         listPos +:= 1
      }

      return results_table
   end

   # Handle a new signature #
   method handleNewSignature(context, results_table)
      local _context, sig_tables := [], methodName, objectName, linked_classes, imported_classes,
         internal_classes, package_classes, objects, className, built_in_functions, linked_procedures,
         imported_procedures, internal_procedures, package_procedures, line, charNum
      
      currentSigItems := []

      # Collect context information (inside comment, space before, object before, no object before) #
      _context := context.getCase()
      line := context.line
      charNum := context.charNum

      /triggerCharLoc := charNum

      linked_classes := filehandler.getLinkedClasses()
      imported_classes := filehandler.getImportedClasses()
      internal_classes := filehandler.getInternalClasses()
      package_classes := filehandler.getPackageClasses()

      case _context of {

         # If object before, collect object information #
         "object" : {

            objectName := context.objectName
            methodName := context.methodName

            objects := filehandler.getObjects()
            className := objects[objectName]

            # Check if function is a method of a linked class #
            if member(linked_classes, className) & member(linked_classes[className]["methods"], methodName) then {
               put(sig_tables, updateResults(methodName, linked_classes[className]["methods"][methodName], results_table))
               linked_classes[className]["methods"][methodName]["name"] := methodName
               linked_classes[className]["methods"][methodName]["type"] := "method"
               put(currentSigItems, linked_classes[className]["methods"][methodName])
            }

            # Check if function is a method of an imported class #
            if member(imported_classes, className) & member(imported_classes[className]["methods"], methodName) then {
               put(sig_tables, updateResults(methodName, imported_classes[className]["methods"][methodName], results_table))
               imported_classes[className]["methods"][methodName]["name"] := methodName
               imported_classes[className]["methods"][methodName]["type"] := "method"
               put(currentSigItems, imported_classes[className]["methods"][methodName])
            }

            # Check if function is a method of an internal class #
            if member(internal_classes, className) & member(internal_classes[className]["methods"], methodName) then {
               put(sig_tables, updateResults(methodName, internal_classes[className]["methods"][methodName], results_table))
               internal_classes[className]["methods"][methodName]["name"] := methodName
               internal_classes[className]["methods"][methodName]["type"] := "method"
               put(currentSigItems, internal_classes[className]["methods"][methodName])
            }

            # Check if function is a method of a package class #
            if member(package_classes, className) & member(package_classes[className]["methods"], methodName) then {
               put(sig_tables, updateResults(methodName, package_classes[className]["methods"][methodName], results_table))
               package_classes[className]["methods"][methodName]["name"] := methodName
               package_classes[className]["methods"][methodName]["type"] := "method"
               put(currentSigItems, package_classes[className]["methods"][methodName])
            }
         }

         # If no object before, treat as method inside class, procedures, or built-in-funtion #
         default : {

            methodName := context.methodName

            built_in_functions := filehandler.getBuiltInFunctions()
            linked_procedures := filehandler.getLinkedProcedures()
            imported_procedures := filehandler.getImportedProcedures()
            internal_procedures := filehandler.getInternalProcedures()
            package_procedures := filehandler.getPackageProcedures()

            # Check if function is an internal procedure #
            if member(internal_procedures, methodName) then {
               put(sig_tables, updateResults(methodName, internal_procedures[methodName], results_table))
               internal_procedures[methodName]["name"] := methodName
               internal_procedures[methodName]["type"] := "procedure"
               put(currentSigItems, internal_procedures[methodName])
            }

            # Check if function is a package procedure #
            if member(package_procedures, methodName) then {
               put(sig_tables, updateResults(methodName, package_procedures[methodName], results_table))
               package_procedures[methodName]["name"] := methodName
               package_procedures[methodName]["type"] := "procedure"
               put(currentSigItems, package_procedures[methodName])
            }

            # Check if function is a linked procedure #
            if member(linked_procedures, methodName) then {
               put(sig_tables, updateResults(methodName, linked_procedures[methodName], results_table))
               linked_procedures[methodName]["name"] := methodName
               linked_procedures[methodName]["type"] := "procedure"
               put(currentSigItems, linked_procedures[methodName])
            }

            # Check if function is an imported procedure #
            if member(imported_procedures, methodName) then {
               put(sig_tables, updateResults(methodName, imported_procedures[methodName], results_table))
               imported_procedures[methodName]["name"] := methodName
               imported_procedures[methodName]["type"] := "procedure"
               put(currentSigItems, imported_procedures[methodName])
            }

            # Check if function is a built in function #
            if member(built_in_functions, methodName) then {
               put(sig_tables, updateResults(methodName, built_in_functions[methodName], results_table))
               built_in_functions[methodName]["name"] := methodName
               built_in_functions[methodName]["type"] := "built-in"
               put(currentSigItems, built_in_functions[methodName])
            }

            # Check if function is part of an internal class #
            every className := key(internal_classes) do {
               if member(internal_classes[className]["methods"], methodName) then {
                  put(sig_tables, updateResults(methodName, internal_classes[className]["methods"][methodName], results_table))
                  internal_classes[className]["methods"][methodName]["name"] := methodName
                  internal_classes[className]["methods"][methodName]["type"] := "method"
                  put(currentSigItems, internal_classes[className]["methods"][methodName])
               }
            }

            # Check if function is a constructor #
            if member(linked_classes, methodName) then {
               put(sig_tables, updateResults(methodName, linked_classes[methodName], results_table))
               linked_classes[methodName]["name"] := methodName
               linked_classes[methodName]["type"] := "class"
               put(currentSigItems, linked_classes[methodName])
            }
            if member(imported_classes, methodName) then {
               put(sig_tables, updateResults(methodName, imported_classes[methodName], results_table))
               imported_classes[methodName]["name"] := methodName
               imported_classes[methodName]["type"] := "class"
               put(currentSigItems, imported_classes[methodName])
            }
            if member(internal_classes, methodName) then {
               put(sig_tables, updateResults(methodName, internal_classes[methodName], results_table))
               internal_classes[methodName]["name"] := methodName
               internal_classes[methodName]["type"] := "class"
               put(currentSigItems, internal_classes[methodName])
            }
            if member(package_classes, methodName) then {
               put(sig_tables, updateResults(methodName, package_classes[methodName], results_table))
               package_classes[methodName]["name"] := methodName
               package_classes[methodName]["type"] := "class"
               put(currentSigItems, package_classes[methodName])
            }
         }
      }

      if \params & params["context"]["isRetrigger"] == "__false__" then
         handleActiveSignature(params, results_table, context)

      return sig_tables
   end

   # Handle an active signature #
   method handleActiveSignature(params, results_table, context)
      local depth := 0, new_signatures := [], position, signature, new_signature, i, temp, line, charNum, temp_context
      static signatures
      initial signatures := []

      if \context then {
         line := context.line
         charNum := context.charNum
      }

      # Check if cursor position is before trigger character #
      if string(params) == "reset" | /triggerCharLoc | (charNum < triggerCharLoc) then {
         triggerCharLoc := &null
         signatures := []
         fail
      }

      # If first call, put initial signature in signatures #
      if *signatures = 0 then {
         every signature := !results_table["signatures"] do
            put(signatures, signature)
         fail
      }

      # Handle any new signatures #
      if line[charNum] == "(" then {
         new_signatures |||:= handleNewSignature(context, results_table)
      }

      # Handle any new signatures while back tracking #
      if line[charNum + 1] == ")" then {
         line ? {
            &pos := charNum
            depth := 0
            until line[&pos] == "(" & depth = 0 do {
               if line[&pos] == ")" then depth +:= 1
               else if line[&pos] == "(" then depth -:= 1
               if &pos = 1 then break
               move(-1)
            }

            temp_context := filehandler.getContext(params["position"]["line"] + 1, &pos)
            new_signatures |||:= handleNewSignature(temp_context, results_table)
         }
      }

      # Remove any duplicate signatures #
      position := 1
      every new_signature := !new_signatures do {
         every signature := !signatures do {
            if new_signature["label"] == signature["label"] &
               *new_signature["parameters"] = *signature["parameters"] then
                  delete(new_signatures, position)
         }
         position +:= 1
      }

      # Add new signatures #
      every new_signature := !new_signatures do {
         put(signatures, new_signature)
      }

      # Set every signatures active parameter to 0 #
      every i := 1 to *signatures do {
         temp := signatures[i]
         temp["activeParameter"] := 0
         signatures[i] := temp
      }

      # Determine depth and active parameter for each active signature #
      line ? {
         &pos := triggerCharLoc
         until &pos = charNum do {
            move(1)
            if line[&pos] == "(" then {
               depth +:= 1
            }
            else if line[&pos] == ")" then {
               signatures[depth + 1]["activeParameter"] := 0
               depth -:= 1
            }
            else if line[&pos] == "," then {
               signatures[depth + 1]["activeParameter"] +:= 1
            }
         }
      }

      # Remove any signatures greater than the depth at cursor location #
      if (depth + 1) < *signatures then {
         delete(signatures, *signatures)
      }

      # Fail if outside of initial bounds #
      if depth < 0 then {
         triggerCharLoc := &null
         signatures := []
         fail
      }

      # Return results #
      results_table["activeSignature"] := *signatures - 1
      results_table["signatures"] := signatures
   end

   # Update the results table #
   method updateResults(function, functionTable, results_table)
      local signature, param_list := [], signature_table, param_table

      if \functionTable["constructor"] then {
         functionTable := functionTable["constructor"]
         function := functionTable["name"]
      }

      # Create a valid signature #
      signature := createSignature(function, functionTable)
      signature := replace(signature, "\"", "\\\"")
      signature := replace(signature, "\\n", "\\\\n")

      # Format parameters #
      every param_table := !functionTable["params"] do
         put(param_list, table("label", param_table["name"]))

      if member(functionTable, "comments") then
         signature_table := table("label", signature, "parameters", param_list, "activeParameter", 0,
            "documentation", table("kind", "markdown", "value", functionTable["comments"]))
      else
         signature_table := table("label", signature, "parameters", param_list, "activeParameter", 0)

      # Update results #
      if \results_table then put(results_table["signatures"], signature_table)
      return signature_table
   end

   # Create a valid signature #
   method createSignature(function, functionTable)
      local signature, counter, param_table

      counter := 0
      signature := function || "("

      every param_table := !functionTable["params"] do {
         signature ||:= param_table["name"]
         if \param_table["type"] then signature ||:= ":" || param_table["type"]
         if \param_table["default"] then signature ||:= ":" || param_table["default"]
         counter +:= 1
         if counter < *functionTable["params"] then signature ||:= ", "
      }

      signature ||:= ")"

      return signature
   end

   method getCurrentSigItems()
      return currentSigItems
   end
end