# -*- coding: utf-8 -*-
module Sablon
  class Processor
    def self.process(xml_node, context, properties = {})
      processor = new(parser)
      stringified_context = Hash[context.map {|k, v| [k.to_s, v] }]
      processor.manipulate xml_node, stringified_context
      processor.write_properties xml_node, properties if properties.any?
      xml_node
    end

    def self.parser
      @parser ||= Sablon::Parser::MailMerge.new
    end

    def initialize(parser)
      @parser = parser
    end

    def manipulate(xml_node, context)
      operations = build_operations(@parser.parse_fields(xml_node))
      operations.each do |step|
        step.evaluate context
      end
      cleanup(xml_node)
      xml_node
    end

    def write_properties(xml_node, properties)
      if properties.key? :start_page_number
        section_properties = SectionProperties.from_document(xml_node)
        section_properties.start_page_number = properties[:start_page_number]
      end
    end

    private
    def build_operations(fields)
      OperationConstruction.new(fields).operations
    end

    def cleanup(xml_node)
      fill_empty_table_cells xml_node
    end

    def fill_empty_table_cells(xml_node)
      xml_node.xpath("//w:tc[count(*[name() = 'w:p'])=0 or not(*)]").each do |blank_cell|
        filler = Nokogiri::XML::Node.new("w:p", xml_node.document)
        blank_cell.add_child filler
      end
    end

    class Block < Struct.new(:start_field, :end_field)
      def self.enclosed_by(start_field, end_field)
        @blocks ||= [RowBlock, ParagraphBlock]
        block_class = @blocks.detect { |klass| klass.encloses?(start_field, end_field) }
        block_class.new start_field, end_field
      end

      def process(context)
        replaced_node = Nokogiri::XML::Node.new("tmp", start_node.document)
        replaced_node.children = Nokogiri::XML::NodeSet.new(start_node.document, body.map(&:dup))
        Processor.process replaced_node, context
        replaced_node.children
      end

      def replace(content)
        content.each { |n| start_node.add_next_sibling n }

        body.each &:remove
        start_node.remove
        end_node.remove
      end

      def body
        return @body if defined?(@body)
        @body = []
        node = start_node
        while (node = node.next_element) && node != end_node
          @body << node
        end
        @body
      end

      def start_node
        @start_node ||= self.class.parent(start_field).first
      end

      def end_node
        @end_node ||= self.class.parent(end_field).first
      end

      def self.encloses?(start_field, end_field)
        parent(start_field).any? && parent(end_field).any?
      end
    end

    class RowBlock < Block
      def self.parent(node)
        node.ancestors ".//w:tr"
      end

      def self.encloses?(start_field, end_field)
        if super
          parent(start_field) != parent(end_field)
        end
      end
    end

    class ParagraphBlock < Block
      def self.parent(node)
        node.ancestors ".//w:p"
      end
    end

    class OperationConstruction
      def initialize(fields)
        @fields = fields
        @operations = []
      end

      def operations
        while @fields.any?
          @operations << consume(true)
        end
        @operations.compact
      end

      def consume(allow_insertion)
        @field = @fields.shift
        return unless @field
        case @field.expression
        when /^=/
          if allow_insertion
            Statement::Insertion.new(Expression.parse(@field.expression[1..-1]), @field)
          end
        when /([^ ]+):each\(([^ ]+)\)/
          block = consume_block("#{$1}:endEach")
          Statement::Loop.new(Expression.parse($1), $2, block)
        when /([^ ]+):if\(([^)]+)\)/
          block = consume_block("#{$1}:endIf")
          Statement::Condition.new(Expression.parse($1), block, $2)
        when /([^ ]+):if/
          block = consume_block("#{$1}:endIf")
          Statement::Condition.new(Expression.parse($1), block)
        end
      end

      def consume_block(end_expression)
        start_field = end_field = @field
        while end_field && end_field.expression != end_expression
          consume(false)
          end_field = @field
        end

        if end_field
          Block.enclosed_by start_field, end_field
        else
          raise TemplateError, "Could not find end field for «#{start_field.expression}». Was looking for «#{end_expression}»"
        end
      end
    end
  end
end