module Fragments
  class Create < Mutations::Command
    BAD_AST = "Node 0 must be present. Node 0 must be a 'nothing' node."
    BLANK = ""
    BODY = "__body"
    ENTRY = "internal_entry_point"
    INSTANCE = "instance"
    KIND = "__KIND__"
    NAME = "name"
    NEXT = "__next"
    NOTHING = "nothing"
    COMMENT = "__COMMENT__"
    PARENT = "__parent"
    US = "__"
    H = CeleryScript::HeapAddress
    KINDS = CeleryScriptSettingsBag::Corpus.as_json[:nodes].pluck(NAME).sort.map(&:to_s)
    required do
      duck :owner, methods: [:device, :fragment_owner?]
      array :flat_ast do
        hash { duck :*, methods: [] }
      end
    end

    def execute
      # LEGACY SHIM:
      #  1. CelerySlicer inserts a `nothing` node at the start of the flat_ast
      #  2. The new Fragment table expects an ENTRY node at position 0 of the
      #     flat_ast
      #  3. This is the fast solution. When all resources switch to the new
      #     `Fragment` table, we can modify the Slicer to use ENTRY at pos 0.
      #  TODO: Store sequence ASTs in fragment table, eventually.
      #          -RC 12 DEC 18
      nodes[0] = entry_node # <= THIS MATTERS
      flat_ast.each_with_index do |flat_node, index|
        node = nodes.fetch(index)
        flat_node.without(KIND).map do |(k, v)|
          if k.starts_with?(US)
            case k
            when PARENT then nodes.fetch(index).parent = nodes.fetch(v.value)
            when NEXT then nodes.fetch(index).next = nodes.fetch(v.value)
            when BODY then nodes.fetch(index).body = nodes.fetch(v.value)
            when COMMENT then node.comment = flat_node.fetch(COMMENT)
            else
              arg_name = ArgName.cached_by_value(k.gsub(US, BLANK))
              new_standard_pair(fragment: fragment,
                                arg_set: node.arg_set,
                                arg_name: arg_name,
                                node: nodes.fetch(flat_node.fetch(k).value))
            end
          end
        end
      end

      entry_node.next = nodes.fetch(1)

      Node.transaction do
        nodes.map(&:save!)
        primitive_pairs.map(&:save!)
        standard_pairs.map(&:save!)
      end
      fragment
    end

    def nodes
      @nodes ||= flat_ast.map do |flat_node|
        kind = Kind.cached_by_value(flat_node.fetch(KIND))
        real_node = Node.new(kind: kind,
                             fragment: fragment,
                             parent: entry_node,
                             next: entry_node,
                             body: entry_node)
        arg_set = ArgSet.new(node: real_node, fragment: fragment)
        flat_node.without(KIND, INSTANCE).map do |(k, v)|
          if !k.starts_with?(US)
            arg_name = ArgName.cached_by_value(k.gsub(US, BLANK))
            primitive = primitives.fetch(v) do
              primitives[v] = Primitive.find_or_create_by(value: v,
                                                          fragment: fragment)
            end
            new_primitive_pair(arg_name: arg_name,
                               arg_set: arg_set,
                               primitive: primitive,
                               fragment: fragment)
          end
        end
        real_node
      end
    end

    def new_standard_pair(args)
      standard_pairs.push(StandardPair.new(args))
    end

    def new_primitive_pair(args)
      primitive_pairs.push(PrimitivePair.new(args))
    end

    def primitive_pairs
      @primitive_pairs ||= []
    end

    def standard_pairs
      @standard_pairs ||= []
    end

    def primitives
      @primitives ||= {}
    end

    def fragment
      @fragment ||= Fragment.new(device: owner.device, owner: owner)
    end

    def entry_node
      @entry_node ||= Node.new(kind: Kind.cached_by_value(ENTRY),
                               fragment: fragment)
    end
  end
end
