module Origen
  class Generator
    # Handles the recursive rendering and importing of sub templates
    # and source files
    module Renderer
      def render(file, options = {}, &block)
        fail 'File argument is nil' unless file
        file = Origen.file_handler.clean_path_to_sub_template(file)
        current_pipeline << { file: file, options: options,
                              placeholder: placeholder, block: block,
                              indent: options[:indent] || 0
                            }
        if block_given?
          self.current_buffer += current_pipeline.last[:placeholder] + "\n"
        end
        current_pipeline.last[:placeholder]
      end
      alias_method :import, :render

      def placeholder
        @ix ||= 0
        @ix += 1
        "_origen_render_placeholder_#{@ix}"
      end

      def options
        @current_options ||= {}
      end

      def pipeline
        @pipeline ||= []
        @pipeline << [] if @pipeline.empty?
        @pipeline
      end

      def current_pipeline
        pipeline.last
      end

      # Insert rendered content into any placeholders
      def insert(content)
        while current_pipeline.size > 0
          current = current_pipeline.pop
          pipeline << []
          @current_options = current[:options]
          self.current_buffer = ''
          output = compile(current[:file],
                           sub_template: true,
                           block:        current[:block],
                           scope:        @scope
                          )
          if current[:indent] && current[:indent] > 0
            indent = ' ' * current[:indent]
            output = output.split("\n").map { |l| indent + l }.join("\n")
          end
          @current_options = nil
          content = insert_content(content, current[:placeholder], output)
        end
        pipeline.pop
        # Always give back a string, this is what existing callers expect
        #
        # Possible this could in future run into problems if the whole file cannot be read
        # into memory, but we can cross that path when we come to it
        if content.is_a?(Pathname)
          c = content.read
          content.delete
          c
        else
          content
        end
      end

      def insert_content(current, placeholder, content)
        # Start using the disk for storing the output rather than memory
        # once it starts to exceed this length
        max_length = 1_000_000
        if current.is_a?(Pathname) || content.is_a?(Pathname) ||
           ((current.length + content.length) > max_length)
          unless current.is_a?(Pathname)
            t = temporary_file
            t.open('w') { |f| f.puts current }
            current = t
          end
          new = temporary_file
          new.open('w') do |new_f|
            current.each_line do |line|
              if line.strip == placeholder
                if content.is_a?(Pathname)
                  content.each_line do |line|
                    new_f.puts line
                  end
                  content.delete
                else
                  new_f.puts content.chomp
                end
              else
                new_f.puts line
              end
            end
          end
          current.delete
          new
        else
          current.sub(/ *#{placeholder}/, content)
        end
      end

      # Returns a Pathname to a uniquely named temporary file
      def temporary_file
        # Ensure this is unique so that is doesn't clash with parallel compile processes
        Pathname.new "#{Origen.root}/tmp/compiler_#{Process.pid}_#{Time.now.to_f}"
      end
    end
  end
end
