require "erb"

require "rubygems"
require "color/rgb/jp"


module Color::RGB::JP
  module Compiler; end
end

class Color::RGB::JP::Compiler::Compiler

  COLOR_NAME_CHAR = /[^[:space:][:punct:][:cntrl:]]/
  COLOR_NAME_RE = /#{COLOR_NAME_CHAR}+(?:-#{COLOR_NAME_CHAR}+)*/

  DEFAULT_PREPROCESSOR = [:preprocess_alias, :preprocess_color]
  DEFAULT_TRIM_MODE = ">%"

  attr_accessor :pallet, :encoding, :prefix, :generator, :preprocessor, :trim_mode

  def initialize(pallet, encoding, prefix, generator)
    @pallet = pallet
    @encoding = encoding
    @prefix = prefix
    @generator = generator
    @preprocessor = DEFAULT_PREPROCESSOR
    @trim_mode = DEFAULT_TRIM_MODE
  end

  def execute(input, output, do_compile)
    with_input_stream(input) do |is|
      erbscript = preprocess(is.read, input)
      with_output_stream(output) do |os|
        if do_compile
          os.write compile(erbscript, input)
        else
          os.write erbscript
        end
      end
    end
  end

  def preprocess(src, input)
    preprocess_snippet(prefix) + do_preprocess(src, @prefix)
  end

  def compile(src, input)
    e = ERB.new(src, 0, @trim_mode)
    e.filename = input if String === input
    e.result
  end

  private

  def do_preprocess(src, prefix)
    @preprocessor.inject(src){|r,fn|
      if Proc === fn
        fn.call(self, r, prefix) || r
      else
        self.__send__(fn, r, prefix) || r
      end
    }
  end

  def preprocess_alias(src, prefix)
    scan_erb(src, /#{prefix}\s*alias[ \t]*((?:#{COLOR_NAME_RE}| )*)/, "<%"){|m|
      n, v = m.strip.split(/\s+/, 2)
      "Color::RGB::JP::ERBUtil.alias(%q{#{n}}, %Q{#{v}})"
    }
  end

  def preprocess_color(src, prefix)
    scan_erb(src, /#{prefix}(#{COLOR_NAME_RE})/, "<%="){|m|
      "Color::RGB::JP::ERBUtil.to_rgb(%q{#{m}})"
    }
  end

  def scan_erb(src, re, embed_mode, &block)
    context = :text
    src.gsub(/(<%)|(%>)|(^%)|(\n|$)|#{re}/) {
      tokens = [:stag, :etag, :percent, :lf, :value].zip(Regexp.last_match.captures)
      context, r = process_token(context, embed_mode, *tokens.find{|e| e[1]}, &block)
      r
    }
  end

  def process_token(context, embed_mode, token, value)
    case context
    when :erb1
      case token
      when :value;    [context, embed_erb(yield(value))]
      when :lf;       [:text, value]
      else            [context, value]
      end
    when :erbtag
      case token
      when :value;    [context, embed_erb(yield(value))]
      when :etag;     [:text, value]
      else            [context, value]
      end
    when :text
      case token
      when :value;    [context, embed_erb(yield(value), embed_mode)]
      when :stag;     [:erbtag, value]
      when :percent;  [:erb1, value]
      else            [context, value]
      end
    else
      raise "[bug] unknown context `#{context}': token=#{token}, value=#{value}"
    end
  end

  def embed_erb(str, tag = nil)
    if tag.nil?
      str
    else
      "#{tag} #{str} %>"
    end
  end

  def preprocess_snippet(prefix)
    return <<-SNIPPET.gsub(/^    /, "")
    <%
    #
    # Auto generated by "#{@generator}".
    # Date: #{Time.now.iso8601}.
    #

    require "color/rgb/jp"

    $KCODE = "#{@encoding}"

    class Color::RGB
      undef to_s
      alias to_s html
    end

    module Color::RGB::JP::ERBUtil
      _pallet = ::#{@pallet.name}
      _alias = {}

      define_method(:pallet) do
        _pallet
      end
      module_function :pallet

      define_method(:alias) do |name, value|
        _alias[name] = value
      end
      module_function :alias

      define_method(:alias?) do |name|
        _alias.key?(name)
      end
      module_function :alias?

      define_method(:to_rgb) do |name|
        c = lookup(name)
        unless c
          msg = "undefined color name: `\#{name}'"
          msg << " (alias of '\#{resolve_alias(name)}')" if alias?(name)
          warn msg
          return "#{prefix}\#{name}"
        end
        c.rgb
      end
      module_function :to_rgb

      define_method(:resolve_alias) do |name|
        if _alias.key?(name)
          resolve_alias(_alias[name])
        else
          name
        end
      end
      module_function :resolve_alias

      define_method(:lookup) do |name|
        _pallet[resolve_alias(name), "#{@encoding}"]
      end
      module_function :lookup
    end
    %>
    SNIPPET
  end

  def with_input_stream(stream)
    if stream.respond_to?(:read)
      yield stream
    else
      open(stream) {|s| yield s }
    end
  end

  def with_output_stream(stream)
    if stream.respond_to?(:write)
      yield stream
    else
      open(stream, "w") {|s| yield s }
    end
  end

end
