require 'html5lib/constants'

#XXX - TODO; make the default interface more ElementTree-like rather than DOM-like

module HTML5lib

  # The scope markers are inserted when entering buttons, object elements,
  # marquees, table cells, and table captions, and are used to prevent formatting
  # from "leaking" into tables, buttons, object elements, and marquees.
  Marker = nil

  module TreeBuilders
    module Base

      class Node
        # The parent of the current node (or nil for the document node)
        attr_accessor :parent

        # a list of child nodes of the current node. This must 
        # include all elements but not necessarily other node types
        attr_accessor :childNodes

        # A list of miscellaneous flags that can be set on the node
        attr_accessor :_flags

        def initialize(name)
          @parent = nil
          @childNodes = []
          @_flags = []
        end

        # Insert node as a child of the current node
        def appendChild(node)
          raise NotImplementedError
        end

        # Insert data as text in the current node, positioned before the 
        # start of node insertBefore or to the end of the node's text.
        def insertText(data, insertBefore=nil)
          raise NotImplementedError
        end

        # Insert node as a child of the current node, before refNode in the 
        # list of child nodes. Raises ValueError if refNode is not a child of 
        # the current node
        def insertBefore(node, refNode)
          raise NotImplementedError
        end

        # Remove node from the children of the current node
        def removeChild(node)
          raise NotImplementedError
        end

        # Move all the children of the current node to newParent. 
        # This is needed so that trees that don't store text as nodes move the 
        # text in the correct way
        def reparentChildren(newParent)
          #XXX - should this method be made more general?
          @childNodes.each { |child| newParent.appendChild(child) }
          @childNodes = []
        end

        # Return a shallow copy of the current node i.e. a node with the same
        # name and attributes but with no parent or child nodes
        def cloneNode
          raise NotImplementedError
        end

        # Return true if the node has children or text, false otherwise
        def hasContent
          raise NotImplementedError
        end
      end

      # Base treebuilder implementation
      class TreeBuilder

        attr_accessor :openElements

        attr_accessor :activeFormattingElements

        attr_accessor :document

        attr_accessor :headPointer

        attr_accessor :formPointer

        # Class to use for document root
        documentClass = nil

        # Class to use for HTML elements
        elementClass = nil

        # Class to use for comments
        commentClass = nil

        # Class to use for doctypes
        doctypeClass = nil

        # Fragment class
        fragmentClass = nil

        def initialize
          reset
        end

        def reset
          @openElements = []
          @activeFormattingElements = []

          #XXX - rename these to headElement, formElement
          @headPointer = nil
          @formPointer = nil

          self.insertFromTable = false

          @document = @documentClass.new
        end

        def elementInScope(target, tableVariant=false)
          # Exit early when possible.
          return true if @openElements[-1].name == target

          # AT How about while true and simply set node to [-1] and set it to
          # [-2] at the end...
          @openElements.reverse.each do |element|
            if element.name == target
              return true
            elsif element.name == 'table'
              return false
            elsif not tableVariant and SCOPING_ELEMENTS.include?(element.name)
              return false
            elsif element.name == 'html'
              return false
            end
          end
          assert false # We should never reach this point
        end

        def reconstructActiveFormattingElements
          # Within this algorithm the order of steps described in the
          # specification is not quite the same as the order of steps in the
          # code. It should still do the same though.

          # Step 1: stop the algorithm when there's nothing to do.
          return if @activeFormattingElements.empty?

          # Step 2 and step 3: we start with the last element. So i is -1.
          i = -1
          entry = @activeFormattingElements[i]
          return if entry == Marker or @openElements.include?(entry)

          # Step 6
          until entry == Marker or @openElements.include?(entry)
            # Step 5: let entry be one earlier in the list.
            i -= 1
            begin
              entry = @activeFormattingElements[i]
            rescue
              # Step 4: at this point we need to jump to step 8. By not doing
              # i += 1 which is also done in step 7 we achieve that.
              break
            end
          end
          while true
            # Step 7
            i += 1

            # Step 8
            clone = @activeFormattingElements[i].cloneNode

            # Step 9
            element = insertElement(clone.name, clone.attributes)

            # Step 10
            @activeFormattingElements[i] = element

            # Step 11
            break if element == @activeFormattingElements[-1]
          end
        end

        def clearActiveFormattingElements
          {} until @activeFormattingElements.empty? || @activeFormattingElements.pop == Marker
        end

        # Check if an element exists between the end of the active
        # formatting elements and the last marker. If it does, return it, else
        # return false
        def elementInActiveFormattingElements(name)
          @activeFormattingElements.reverse.each do |element|
            # Check for Marker first because if it's a Marker it doesn't have a
            # name attribute.
            break if element == Marker
            return element if element.name == name
          end
          return false
        end

        def insertDoctype(name)
          @document.appendChild(@doctypeClass.new(name))
        end

        def insertComment(data, parent=nil)
          parent = @openElements[-1] if parent.nil?
          parent.appendChild(@commentClass.new(data))
        end
               
        # Create an element but don't insert it anywhere
        def createElement(name, attributes)
          element = @elementClass.new(name)
          element.attributes = attributes
          return element
        end

        # Switch the function used to insert an element from the
        # normal one to the misnested table one and back again
        def insertFromTable=(value)
          @insertFromTable = value
          @insertElement = value ? :insertElementTable : :insertElementNormal
        end

        def insertElement(name, attributes)
          send(@insertElement, name, attributes)
        end

        def insertElementNormal(name, attributes)
          element = @elementClass.new(name)
          element.attributes = attributes
          @openElements[-1].appendChild(element)
          @openElements.push(element)
          return element
        end

        # Create an element and insert it into the tree
        def insertElementTable(name, attributes)
          element = @elementClass.new(name)
          element.attributes = attributes
          if TABLE_INSERT_MODE_ELEMENTS.include?(@openElements[-1].name)
            #We should be in the InTable mode. This means we want to do
            #special magic element rearranging
            parent, insertBefore = getTableMisnestedNodePosition
            if insertBefore.nil?
              parent.appendChild(element)
            else
              parent.insertBefore(element, insertBefore)
            end
            @openElements.push(element)
          else
            return insertElementNormal(name, attributes)
          end
          return element
        end

        def insertText(data, parent=nil)
          parent = @openElements[-1] if parent.nil?

          if (not(@insertFromTable) or (@insertFromTable and not TABLE_INSERT_MODE_ELEMENTS.include?(@openElements[-1].name)))
            parent.insertText(data)
          else
            #We should be in the InTable mode. This means we want to do
            #special magic element rearranging
            parent, insertBefore = getTableMisnestedNodePosition
            parent.insertText(data, insertBefore)
          end
        end
      
        # Get the foster parent element, and sibling to insert before
        # (or nil) when inserting a misnested table node
        def getTableMisnestedNodePosition
          #The foster parent element is the one which comes before the most
          #recently opened table element
          #XXX - this is really inelegant
          lastTable = nil
          fosterParent = nil
          insertBefore = nil
          @openElements.reverse.each do |element|
            if element.name == "table"
              lastTable = element
              break
            end
          end
          if lastTable
            #XXX - we should really check that this parent is actually a
            #node here
            if lastTable.parent
              fosterParent = lastTable.parent
              insertBefore = lastTable
            else
              fosterParent = @openElements[@openElements.index(lastTable) - 1]
            end
          else
            fosterParent = @openElements[0]
          end
          return fosterParent, insertBefore
        end

        def generateImpliedEndTags(exclude=nil)
          name = @openElements[-1].name

          if (['dd', 'dt', 'li', 'p', 'td', 'th', 'tr'].include?(name) and name != exclude)
            @openElements.pop
            # XXX This is not entirely what the specification says. We should
            # investigate it more closely.
            generateImpliedEndTags(exclude)
          end
        end

        def getDocument
          @document
        end
  
        def getFragment
          #assert @innerHTML
          fragment = @fragmentClass.new
          @openElements[0].reparentChildren(fragment)
          return fragment
        end

        # Serialize the subtree of node in the format required by unit tests
        # node - the node from which to start serializing
        def testSerializer(node)
          raise NotImplementedError
        end

      end
    end
  end
end