module Fable
  class Serializer
    def self.convert_to_runtime_objects(objects, skip_last = false)
      last_item = objects.last
      runtime_objects = []

      objects.each do |object|
        next if object == last_item && skip_last
        runtime_objects << convert_to_runtime_object(object)
      end

      runtime_objects
    end

    def self.convert_to_runtime_objects_hash(hash)
      runtime_hash = {}

      hash.each do |key, value|
        runtime_hash[key] = convert_to_runtime_object(value)
      end

      return runtime_hash
    end

    def self.array_to_container(array)
      contents = convert_to_runtime_objects(array, true)
      # Final object in the array is always a combination of
      # - named content
      # - an #f key with the count flags
      final_object = array.last
      bit_flags = 0
      container_name = nil
      named_only_content = {}

      if !final_object.nil?
        final_object.each do |key, value|
          if key == "#f"
            bit_flags = value.to_i
          elsif key == "#n"
            container_name = value
          else
            named_content_item = convert_to_runtime_object(value)
            if named_content_item.is_a?(Container)
              named_content_item.name = key
            end

            named_only_content[key] = named_content_item
          end
        end
      end

      container = Container.new(bit_flags)
      container.add_content(contents)
      if !container_name.nil?
        container.name = container_name
      end

      named_only_content.each do |key, content|
        container.add_to_named_content(content)
      end

      return container
    end

    # ----------------------
    # JSON ENCODING SCHEME
    # ----------------------
    #
    # Glue:           "<>", "G<", "G>"
    #
    # ControlCommand: "ev", "out", "/ev", "du" "pop", "->->", "~ret", "str", "/str", "nop",
    #                 "choiceCnt", "turns", "visit", "seq", "thread", "done", "end"
    #
    # NativeFunction: "+", "-", "/", "*", "%" "~", "==", ">", "<", ">=", "<=", "!=", "!"... etc
    #
    # Void:           "void"
    #
    # Value:          "^string value", "^^string value beginning with ^"
    #                 5, 5.2
    #                 {"^->": "path.target"}
    #                 {"^var": "varname", "ci": 0}
    #
    # Container:      [...]
    #                 [...,
    #                     {
    #                         "subContainerName": ...,
    #                         "#f": 5,                    # flags
    #                         "#n": "containerOwnName"    # only if not redundant
    #                     }
    #                 ]
    #
    # Divert:         {"->": "path.target", "c": true }
    #                 {"->": "path.target", "var": true}
    #                 {"f()": "path.func"}
    #                 {"->t->": "path.tunnel"}
    #                 {"x()": "externalFuncName", "exArgs": 5}
    #
    # Var Assign:     {"VAR=": "varName", "re": true}   # reassignment
    #                 {"temp=": "varName"}
    #
    # Var ref:        {"VAR?": "varName"}
    #                 {"CNT?": "stitch name"}
    #
    # ChoicePoint:    {"*": pathString,
    #                  "flg": 18 }
    #
    # Choice:         Nothing too clever, it's only used in the save state,
    #                 there's not likely to be many of them.
    #
    # Tag:            {"#": "the tag text"}

    def self.convert_to_runtime_object(object)
      case object
      when Numeric
        return Value.create(object)
      when String
        string = object

        # StringValue
        if string.start_with?("^")
          return StringValue.new(string[1..-1])
        elsif string.start_with?("\n") && string.length == 1
          return StringValue.new("\n")
        end

        # Glue
        if string == "<>"
          return Glue.new
        end

        # Control Commands
        if ControlCommand.is_control_command?(string)
          return ControlCommand.get_control_command(string)
        end

        # Native functions
        # "^" conflicts with the way we identify strings, so now
        # we know it's not a string, we can convert back to the proper symbol
        # for this operator
        if string == "L^"
          string = "^"
        end

        if NativeFunctionCall.is_native_function?(string)
          return NativeFunctionCall.new(string)
        end

        # Pop
        if string == ControlCommand::COMMANDS[:POP_TUNNEL]
          return ControlCommand.get_control_command(string)
        elsif string == ControlCommand::COMMANDS[:POP_FUNCTION]
          return ControlCommand.get_control_command(string)
        end

        if string == "void"
          return Void.new
        end
      when Hash
        given_hash = object

        # Divert target value to path
        if given_hash["^->"]
          return DivertTargetValue.new(Path.new(given_hash["^->"]))
        end

        # VariablePointerType
        if given_hash["^var"]
          variable_pointer = VariablePointerValue.new(given_hash["^var"])
          if given_hash["ci"]
            variable_pointer.context_index = given_hash["ci"]
          end

          return variable_pointer
        end

        # Divert
        is_divert = false
        pushes_to_stack = false
        divert_push_pop_type = PushPopType::TYPES[:function]
        external = false
        value = nil

        if given_hash["->"]
          is_divert = true
          value = given_hash["->"]
        elsif given_hash["f()"]
          is_divert = true
          pushes_to_stack = true
          divert_push_pop_type = PushPopType::TYPES[:function]
          value = given_hash["f()"]
        elsif given_hash["->t->"]
          is_divert = true
          pushes_to_stack = true
          divert_push_pop_type = PushPopType::TYPES[:tunnel]
          value = given_hash["->t->"]
        elsif given_hash["x()"]
          is_divert = true
          external = true
          pushes_to_stack = false
          divert_push_pop_type = PushPopType::TYPES[:function]
          value = given_hash["x()"]
        end

        if is_divert
          divert = Divert.new
          divert.pushes_to_stack = pushes_to_stack
          divert.stack_push_type = divert_push_pop_type
          divert.is_external = external
          target = value.to_s

          if given_hash["var"]
            divert.variable_divert_name = target
          else
            divert.target_path_string = target
          end

          divert.is_conditional = given_hash.has_key?("c")

          if external
            if given_hash["exArgs"]
              divert.external_arguments = given_hash["exArgs"]
            end
          end

          return divert
        end

        # Choice
        if given_hash["*"]
          choice = ChoicePoint.new
          choice.path_string_on_choice = given_hash["*"]

          if given_hash["flg"]
            choice.flags = given_hash["flg"]
          end

          return choice
        end

        # Variable Reference
        if given_hash["VAR?"]
          return VariableReference.new(given_hash["VAR?"])
        elsif given_hash["CNT?"]
          read_count_variable_reference = VariableReference.new(nil)
          read_count_variable_reference.path_string_for_count = given_hash["CNT?"]
          return read_count_variable_reference
        end

        # Variable Assignment
        is_variable_assignment = false
        is_global_variable = false

        if given_hash["VAR="]
          variable_name = given_hash["VAR="]
          is_variable_assignment = true
          is_global_variable = true
        elsif given_hash["temp="]
          variable_name = given_hash["temp="]
          is_variable_assignment = true
          is_global_variable = false
        end

        if is_variable_assignment
          is_new_declaration = !given_hash.has_key?("re")
          variable_assignment = VariableAssignment.new(variable_name, is_new_declaration)
          variable_assignment.global = is_global_variable
          return variable_assignment
        end

        # Tag
        if given_hash["#"]
          return Tag.new(given_hash["#"])
        end

        # List
        if given_hash["list"]
          list_content = given_hash["list"]
          raw_list = InkList.new
          if given_hash["origins"]
            raw_list.set_initial_origin_names(given_hash["origins"])
          end

          list_content.each do |key, value|
            item = InkList::InkListItem.new(full_name: key)
            raw_list.list[item] = value.to_i
          end

          return ListValue.new(raw_list)
        end

        # Used when serializing save state only
        if given_hash["originalChoicePath"]
          return object_to_choice(object)
        end
      when Array
        return array_to_container(object)
      when NilClass
        return nil
      end

      raise Error, "Failed to convert to runtime object: #{object}"
    end

    def self.object_to_choice(object)
      choice = Choice.new
      choice.text = object["text"]
      choice.index = object["index"].to_i
      choice.source_path = object["originalChoicePath"]
      choice.original_thread_index = object["originalThreadIndex"].to_i
      choice.path_string_on_choice = object["targetPath"]
      return choice
    end

    def self.convert_to_list_definitions(object)
      all_definitions = []

      object.each do |name, list_definition_hash|
        items = {}
        list_definition_hash.each do |key, value|
          items[key] = value.to_i
        end

        all_definitions << ListDefinition.new(name, items)
      end

      return ListDefinitionsOrigin.new(all_definitions)
    end

    def self.convert_hash_of_runtime_objects(hash_value)
      result = {}
      hash_value.each do |key, value|
        result[key] = convert_from_runtime_object(value)
      end

      result
    end

    def self.convert_array_of_runtime_objects(array)
      array.map{|x| convert_from_runtime_object(x) }
    end

    def self.convert_from_runtime_object(object)
      if object.is_a?(Container)
        return convert_from_container(object)
      end

      if object.is_a?(Divert)
        return convert_from_divert(object)
      end

      if object.is_a?(ChoicePoint)
        return convert_from_choice_point(object)
      end

      if object.is_a?(IntValue) || object.is_a?(FloatValue)
        return object.value
      end

      if object.is_a?(StringValue)
        return convert_from_string_value(object)
      end

      if object.is_a?(ListValue) || object.is_a?(InkList)
        return convert_from_list(object)
      end

      if object.is_a?(DivertTargetValue)
        return convert_divert_target_value(object)
      end

      if object.is_a?(VariablePointerValue)
        return convert_variable_pointer_value(object)
      end

      if object.is_a?(Glue)
        return convert_glue(object)
      end

      if object.is_a?(ControlCommand)
        return convert_control_command(object)
      end

      if object.is_a?(NativeFunctionCall)
        return convert_native_function_call(object)
      end

      if object.is_a?(VariableReference)
        return convert_variable_reference(object)
      end

      if object.is_a?(VariableAssignment)
        return convert_variable_assignment(object)
      end

      if object.is_a?(Void)
        return convert_void
      end

      if object.is_a?(Tag)
        return convert_tag(object)
      end

      if object.is_a?(Choice)
        return convert_choice(object)
      end

      raise ArgumentError, "Failed to serialize runtime object: #{object}"
    end

    def self.convert_from_divert(object)
      result = {}
      divert_type_key = "->"

      if divert.external?
        divert_type_key = "x()"
      elsif divert.pushes_to_stack?
        if divert.stack_push_type == PushPopType::TYPES[:function]
          divert_type_key = "f()"
        else
          divert_type_key = "->t->"
        end
      end

      if divert.has_variable_target?
        target_string = divert.variable_divert_name
      else
        target_string = divert.target_path_string
      end

      result[divert_type_key] = target_string

      if divert.has_variable_target?
        result["var"] = true
      end

      if divert.is_conditional?
        result["c"] = true
      end

      if divert.external_arguments > 0
        result["exArgs"] = divert.external_arguments
      end

      return result
    end

    def self.convert_from_choice_point(choice_point)
      return {
        "*" => choice_point.path_string_on_choice,
        "flg" => choice_point.flags
      }
    end

    def self.convert_from_string_value(string_value)
      if string_value.is_newline?
        return "\n"
      else
        return "^#{string_value.value}"
      end
    end

    def self.convert_from_list(list_value)
      result = {}
      if list_value.is_a?(ListValue)
        raw_list = list_value.value
      else
        raw_list = list_value
      end

      raw_list.list.each do |list_item, value|
        result[list_item.full_name] = value
      end

      if raw_list.list.empty? && !raw_list.origin_names.nil? && !raw_list.origin_names.empty?
        result["origins"] = raw_list.origin_names
      end

      return {"list" => result}
    end

    def self.convert_divert_target_value(divert_target_value)
      return {"^->" => divert_target_value.value.components_string }
    end

    def self.convert_variable_pointer_value(variable_pointer_value)
      return {
        "^var" => variable_pointer_value.value,
        "ci" => variable_pointer_value.context_index
      }
    end

    def self.convert_glue(glue)
      return "<>"
    end

    def self.convert_control_command(control_command)
      return control_command.command_type
    end

    def self.convert_native_function_call(native_function_call)
      function_name = native_function_call.name
      if name == "^"
        return "L^"
      else
        return name
      end
    end

    def self.convert_variable_reference(variable_reference)
      read_count_path = variable_reference.path_string_for_count
      if read_count_path.nil?
        return {"CNT?" => read_count_path}
      else
        return {"VAR?" => variable_reference.name}
      end
    end

    def self.convert_variable_assignment(variable_assignment)
      result = {}

      if variable_assignment.global?
        key = "VAR="
      else
        key = "temp="
      end

      result[key] = variable_assignment.variable_name

      if !variable_assignment.new_declaration?
        result["re"] = true
      end

      return result
    end

    def self.convert_void
      return "void"
    end

    def self.convert_tag(tag)
      return {
        "#" => tag.text
      }
    end

    def self.convert_choice(choice)
      return {
        "text" => choice.text,
        "index" => choice.index,
        "originalChoicePath" => choice.source_path,
        "originalThreadIndex" => choice.original_thread_index,
        "targetPath" => choice.path_string_on_choice
      }
    end

    def self.convert_choices(choices)
      choices.map{|choice| convert_choice(choice) }
    end
  end
end