require 'cgi'
require 'chunks/engines'
require 'chunks/category'
require_dependency 'chunks/include'
require_dependency 'chunks/redirect'
require_dependency 'chunks/wiki'
require_dependency 'chunks/literal'
require 'chunks/nowiki'
require 'sanitizer'
require 'stringsupport'


# Wiki content is just a string that can process itself with a chain of
# actions. The actions can modify wiki content so that certain parts of
# it are protected from being rendered by later actions.
#
# When wiki content is rendered, it can be interrogated to find out
# which chunks were rendered. This means things like categories, wiki
# links, can be determined.
#
# Exactly how wiki content is rendered is determined by a number of
# settings that are optionally passed in to a constructor. The current
# options are:
#  * :engine
#    => The structural markup engine to use (Textile, Markdown, RDoc)
#  * :engine_opts
#    => A list of options to pass to the markup engines (safe modes, etc)
#  * :pre_engine_actions
#    => A list of render actions or chunks to be processed before the
#       markup engine is applied. By default this is:
#       Category, Include, URIChunk, WikiChunk::Link, WikiChunk::Word
#  * :post_engine_actions
#    => A list of render actions or chunks to apply after the markup
#       engine. By default these are:
#       Literal::Pre, Literal::Tags
#  * :mode
#    => How should the content be rendered? For normal display (show),
#       publishing (:publish) or export (:export)?

module ChunkManager
  attr_reader :chunks_by_type, :chunks_by_id, :chunks, :chunk_id

  ACTIVE_CHUNKS = [ NoWiki, Category, Redirect, WikiChunk::Link,
                    WikiChunk::Word ]

  HIDE_CHUNKS = [ Literal::Pre, Literal::Tags, Literal::Math ]

  MASK_RE = {
    ACTIVE_CHUNKS => Chunk::Abstract.mask_re(ACTIVE_CHUNKS),
    HIDE_CHUNKS => Chunk::Abstract.mask_re(HIDE_CHUNKS)
  }

  def init_chunk_manager
    @chunks_by_type = Hash.new
    Chunk::Abstract::derivatives.each{|chunk_type|
      @chunks_by_type[chunk_type] = Array.new
    }
    @chunks_by_id = Hash.new
    @chunks = []
    @chunk_id = 0
  end

  def add_chunk(c)
      @chunks_by_type[c.class] << c
      @chunks_by_id[c.object_id] = c
      @chunks << c
      @chunk_id += 1
  end

  def delete_chunk(c)
    @chunks_by_type[c.class].delete(c)
    @chunks_by_id.delete(c.object_id)
    @chunks.delete(c)
  end

  def merge_chunks(other)
    other.chunks.each{|c| add_chunk(c)}
  end

  def scan_chunkid(text)
    text.scan(MASK_RE[ACTIVE_CHUNKS]){|a| yield a[0] }
  end

  def find_chunks(chunk_type)
    @chunks.select { |chunk| chunk.kind_of?(chunk_type) and chunk.rendered? }
  end

end

# A simplified version of WikiContent. Useful to avoid recursion problems in
# WikiContent.new
class WikiContentStub < String

  attr_reader :web, :options
  include ChunkManager

  def initialize(content, web, options)
    super(content)
    @web = web
    @options = options
    init_chunk_manager
  end

  # Detects the mask strings contained in the text of chunks of type chunk_types
  # and yields the corresponding chunk ids
  # example: content = "chunk123categorychunk <pre>chunk456categorychunk</pre>"
  # inside_chunks(Literal::Pre) ==> yield 456
  def inside_chunks(chunk_types)
    chunk_types.each{|chunk_type|  chunk_type.apply_to(self) }

    chunk_types.each{|chunk_type| @chunks_by_type[chunk_type].each{|hide_chunk|
        scan_chunkid(hide_chunk.text){|id| yield id }
      }
    }
  end
end

class WikiContent < String

  include ChunkManager
  include Sanitizer

  DEFAULT_OPTS = {
    :active_chunks       => ACTIVE_CHUNKS,
    :hide_chunks         => HIDE_CHUNKS,
    :engine              => Engines::MarkdownMML,
    :engine_opts         => [],
    :mode                => :show
  }.freeze

  attr_reader :web, :options, :revision, :not_rendered, :pre_rendered

  # Create a new wiki content string from the given one.
  # The options are explained at the top of this file.
  def initialize(revision, url_generator, options = {})
    @revision = revision
    @url_generator = url_generator
    @web = @revision.page.web

    @options = DEFAULT_OPTS.dup.merge(options)
    @options[:engine] = Engines::MAP[@web.markup]
    @options[:engine_opts] = [:filter_html, :filter_styles] if @web.safe_mode?
    @options[:active_chunks] = (ACTIVE_CHUNKS - [WikiChunk::Word] ) if @web.brackets_only?
    @options[:hide_chunks] = (HIDE_CHUNKS - [Literal::Math] ) unless
                  [Engines::MarkdownMML, Engines::MarkdownPNG].include?(@options[:engine])

    @not_rendered = @pre_rendered = nil

    super(@revision.content)
    init_chunk_manager
    build_chunks
    @not_rendered = String.new(self)
  end

  # Call @web.page_link using current options.
  def page_link(web_name, name, text, link_type)
    web = Web.find_by_name(web_name) || Web.find_by_address(web_name) || @web
    @options[:link_type] = (link_type || :show)
    @url_generator.make_link(@web, name, web, text, @options)
  end

  def build_chunks
    # create and mask Includes and "active_chunks" chunks
    NoWiki.apply_to(self) if @options[:active_chunks].include?(NoWiki)
    Include.apply_to(self)
    @options[:active_chunks].each{|chunk_type| chunk_type.apply_to(self) unless chunk_type == NoWiki}

    # Handle hiding contexts like "pre" and "code" etc..
    # The markup (textile, rdoc etc) can produce such contexts with its own syntax.
    # To reveal them, we work on a copy of the content.
    # The copy is rendered and used to detect the chunks that are inside protecting context
    # These chunks are reverted on the original content string.

    copy = WikiContentStub.new(self, @web, @options)
    @options[:engine].apply_to(copy)

    copy.inside_chunks(@options[:hide_chunks]) do |id|
      @chunks_by_id[id.to_i].revert
    end
  end

  def pre_render!
    unless @pre_rendered
      @chunks_by_type[Include].each{|chunk| chunk.unmask }
      @pre_rendered = String.new(self)
    end
    @pre_rendered
  end

  def render!
    pre_render!
    @options[:engine].apply_to(self)
    as_utf8
    # unmask in one go. $~[1] is the chunk id
    gsub!(MASK_RE[ACTIVE_CHUNKS]) do
      chunk = @chunks_by_id[$~[1].to_i]
      if chunk.nil?
        # if we match a chunkmask that existed in the original content string
        # just keep it as it is
        $~[0]
      else
        chunk.unmask_text
      end
    end
    self.replace xhtml_sanitize(self)
  end

  def page_name
    @revision.page.name
  end

end