#!/usr/bin/ruby
# -*- coding: utf-8 -*-
# Doxygen で出力したページを SourceForge 用に調整するツール
# 併せて、RSS, 更新履歴の生成も行う
# Satofumi KAMIMURA
# $Id: doxyHtmlUpdate.rb 1982 2012-04-05 17:24:02Z satofumi $
#
# \todo RSS のリンクに改行が入るのを修正する

require 'find'
require 'kconv'
require "rss"
# $KCODE = 'UTF8'


# RSS 用の項目データ
$articles = Array.new


# 設定ファイルがないときの処理
if ARGV.size < 1
  print "usage:\n\t" + __FILE__ + " <config file>\n\n"
  exit(1)
end
config_file = ARGV[0]

# 更新履歴のフレーム幅
HistoryWidth = 250

# 設定ファイルの内容読み出し
class ConfigInformation
  attr_reader :index_page, :history_max, :replace_words, :target_page, :strip_path, :history_x, :history_y, :line_size_max, :log_file, :rss_10_file, :rss_20_file, :rss_base, :rss_title, :rss_description, :rss_insert, :find_dox_directory

  def loadReplaceWords(io)
    while line = io.gets
      case line.chomp
      when /^\s*$/
        # 空行までを読み出す
        return

      when /^(.+?)\s(.+)$/
        @replace_words[$1] = $2
      end
    end
  end

  def initialize(file)
    @target_page = nil
    @index_page = nil
    @replace_words = Hash.new
    @strip_path = ''
    @line_size_max = 20
    @log_file = 'history_log.txt'
    @rss_10_file = nil
    @rss_20_file = nil
    @rss_base = 'rss_base_address'
    @rss_title = 'rss title'
    @rss_description = 'rss description'
    @rss_insert = false
    @dox_find_directory = []

    File.open(file, "r:utf-8") { |io|
      while line = io.gets
        case line.chomp
        when /^#/
          next

        when /TargetHtml\s(.+)/
          @target_page = $1.split

        when /IndexHtml\s(.+)/
          @index_page = $1

        when /StripFromExamplesPath\s(.+)/
          @strip_path = $1

        when /IndexHistoryMax\s(\d+)/
          @history_max = $1.to_i

        when /IndexPosition\s(\d+)x(\d+)/
          @history_x = $1.to_i
          @history_y = $2.to_i

        when /ReplaceList/
          loadReplaceWords(io)

        when /HistoryLineMax\s(\d+)/
          @line_size_max = $1.to_i

        when /HistoryLogFile\s(.+)/
          @log_file = $1

        when /OutputRss10\s(.+)/
          @rss_10_file = $1

        when /OutputRss20\s(.+)/
          @rss_20_file = $1

        when /RssBaseUrl\s(.+)/
          @rss_base = $1

        when /RssTitle\s(.+)/
          @rss_title = $1

        when /RssDescription\s(.+)/
          @rss_description = $1

        when /InsertRss\s[Yy][Ee][Ss]/
          @rss_insert = true

        when /FindDoxDirectory\s(.+)/
          @find_dox_directory = $1.split
        end
      end
    }
    @target_page = (@target_page == nil) ? Array.new : @target_page
    @index_page = (@index_page == nil) ? '' : @index_page
  end
end
configs = ConfigInformation.new(config_file)


# ファイルの置換
def updatefile(page, lines)
  File.open(page, 'w:utf-8') { |io|
    io.write(lines)
  }
end


# 文字毎のエンコード
def hexEncode(line, type)

  encoded = ''
  line.each_byte { |ch|
    if ch.chr =~ /[a-zA-Z:]/
      case type
      when 0
        encoded += '&#' + ch.to_s(10) + ';'

      when 1
        encoded += '%' + ch.to_s(16)

      when 2
        encoded += '&#x' + ch.to_s(16) + ';'
      end
    else
      encoded += ch.chr
    end
  }
  encoded
end


# メールアドレスのエンコード
def encodeMailAddress(line)

  if line =~ /(mailto:)(.+)">(.+)/
    hexEncode($1, 0) + hexEncode($2, 1) + '">' + hexEncode($3, 2)
    #$1 + hexEncode($2, 1) + '" >' + hexEncode($3, 2)
  else
    line
  end
end


# ページ毎に置換処理を行う
configs.target_page.each { |page|
  updated = false

  # ファイルの読み出し
  lines = File.read(page).force_encoding("utf-8")

  # メールアドレスの置換
  match_pattern = /<a href="(mailto:[^%].+)<\/a>/
  while lines.match(match_pattern)
    replace = '<a href="' + encodeMailAddress($1) + '</a>'
    lines.sub!(match_pattern, replace)
    updated = true
  end

  # 任意タグの置換
  configs.replace_words.each { |key, words|
    if lines.match(key)
      lines.gsub!(key, words)
      updated = true
    end
  }

  # ファイルの置換
  if updated
    updatefile(page, lines);
    print 'update "' + page + "\"\n"
  end
}


# ページ情報の処理クラス
class PageInformation
  attr_reader :name, :mtime

  def initialize(fname)
    @name = fname
    @mtime = File.mtime(fname).tv_sec
  end
end


# RSS ファイルの生成
def createRss(type, fname, rss_settings)

  rss = RSS::Maker.make(type) do |maker|
    # !!! fname, basename の扱いを確認すべき
    maker.channel.about = rss_settings['base'] + File.basename(fname)
    maker.channel.title = rss_settings['title'].toutf8
    maker.channel.description = rss_settings['description']
    maker.channel.link = rss_settings['base']

    maker.items.do_sort = true

    items_num = 0;
    $articles.each { |article|
      maker.items.new_item do |item|
        # !!! article を代入するように変更する
        item.link = rss_settings['base'] + article['link']
        item.title = article['title']
        item.date = article['date']
        item.description = article['description']
      end

      items_num += 1
      if items_num >= 15
        break
      end
    }
  end

  File.open(fname, 'w:utf-8') { |io|
    io << rss.to_s
  }
end

# 更新履歴メモの作成
def insertUpdateMemo(logfile, line_size_max)

  if !File.exist?(logfile)
    return '';
  end

  out = ''
  File.open(logfile, "r:utf-8") { |io|
    # パースフォーマット
    # -----+-----
    # リンク
    # タイトル
    # 詳細
    # "\n"

    # !!! 実装が適当すぎるのを修正する
    last_date = Time.now
    parse_index = 0
    link = nil
    title = nil
    messages = ''
    while line = io.gets
      case line
      when /---+---/
        out += '<hr align="left" size="1" width="90%">'

      when /^(\d\d\d\d)[\/-](\d\d)[\/-](\d\d)$/
        last_date = Time.local($1, $2, $3)
        if out != ''
          out += '<br>'
        end
        out += $1 + '-' + $2 + '-' + $3
      else

        case parse_index
        when 0
          # リンク
          link = line.chomp
          parse_index += 1

        when 1
          # タイトル
          title = line.chomp
          parse_index += 1

        else
          # 詳細
          messages += line.chomp
        end

        # !!! 現状は、詳細が改行で終わっているときは、処理されない
        if line.chomp == ''
          # 項目のリンク作成
          if link && title
            if messages.chomp == ''
              messages = '説明なし'
            end
            out += '- <a class="el" href="' + link + '">' + title + '</a><br>'
            out += '<div class="log_space">' + messages + '</div><br>'

            # !!! ここでグローバル変数のは避けたい
            date = last_date
            $articles << { 'link' => link, 'title' => title, 'date' => date, 'description' => messages }
          end

          parse_index = 0
          link = nil
          title = nil
          messages = ''
        end
      end
    end
  }

  '<table class="main" border="0" cellpadding="0" cellspacing="0" width="100%">' + '<tbody><tr><td><strong>' + '更新履歴' + '</strong><div class="log_frame">' + out + '<br><br><br><br><br><br><br><br><br><br><br><br> </td></tr></tbody></table>'
end


# 更新ページ履歴の作成
def insertUpdateHistory(history_max, find_path)

  if find_path == nil
    return ""
  end

  # 対象ページの検索を行う
  files = Array.new
  find_path.each { |path|
    Find.find(path) { |name|
      Find.prune if (name =~ /.svn/ || name =~ /output_html/ || name =~ /~$/)
      if ! File.file?(name)
        next
      end
      ext = File.extname(name)
      if ext != '.c' && ext != '.cpp' && ext != '.h' && ext != '.dox'
        next
      end

      files.push(PageInformation.new(name))
    }
  }
  files.sort! { |a, b| a.mtime <=> b.mtime }
  files.reverse!

  # page 情報を新しい順に処理する
  pages_counter = 0
  update_days = Array.new
  files.each { |file|
    # ファイル毎に \page の情報を取得する
    lines = File.read(file.name).force_encoding("utf-8")
    lines.each_line { |line|
      if line =~ /^\s+\\page\s+(.+?)\s+(.+)/
        link = $1
        title = $2
        pages_counter += 1

        # 更新履歴のタグを作る
        date = Time.at(file.mtime).strftime("%Y-%m-%d")
        href = '<a class=el href="' + link + '.html">' + title + '</a>'
        update_days.push([date, href])
      end
    }
    break if pages_counter >= history_max
  }

  # 整形して返す
  last_date = ''
  output = ''
  update_days.each { |date, href|
    if last_date != date
      if output != ''
        output += '<br>';
      end
      output += date + '<hr align="left" size="1" width="90%">'
      last_date = date
    end
    output += '&nbsp;&nbsp;- ' + href + '<br>'
  }

  return '<strong>' + '最近変更されたページ' + '</strong><br><div class="history_frame">' + output + '</div>'
end


[ configs.index_page ].each { |page|
  unless page.empty?
    lines = File.read(page).force_encoding("utf-8")

    # トップページに更新履歴を追加する
    first_detected = false
    replace_lines = ''
    lines.each_line { |line|
      # 本文中に、"TWOCOLUMN" があれば、それを２段組の開始位置とする
      # Doxygen 処理後は、"&lt;twocolumn&gt;<ul>" になっている
      if (! first_detected) && (line =~ /TWOCOLUMN/)
        first_detected = true
        replace_lines += "<table width=\"100%\"><tbody><tr valign=\"top\"><td valign=\"top\" width=\"60%\">"
        line = '';
      end

      # TWOCOLUMN_END までの行を生成ページの末尾と判断する
      if line =~ /TWOCOLUMN_END/ && first_detected
        line.sub!(/TWOCOLUMN_END/, '')
        replace_lines += "</td><td valign=\"top\" width=\"40%\">"

        # 更新メッセージの追加
        replace_lines += insertUpdateMemo(configs.log_file,
                                          configs.line_size_max)

        # ページ更新履歴の追加
        if configs.history_max > 0
          # 項目を表示する可能性があるならば、表示させる
          replace_lines += '<br><br>'
          replace_lines += insertUpdateHistory(configs.history_max, configs.find_dox_directory)
        end
        replace_lines += "</td></tr></tbody></table>"
      end
      replace_lines += line.chomp
    }

    # TWOCOLUMN でない場合に、RSS を追加するために、insertUpdateMemo を呼び出す
    if ! first_detected
      insertUpdateMemo(configs.log_file, configs.line_size_max)
    end

    # 指定があれば、RSS ファイルを出力する
    rss_settings = { 'base' => configs.rss_base, 'title' => configs.rss_title, 'description' => configs.rss_description }

    if configs.rss_10_file
      createRss('1.0', configs.rss_10_file, rss_settings)
      if configs.rss_insert
        replace_lines.sub!(/<\/title>/, '</title><link rel="alternate" type="application/rss+xml" title="RSS 1.0" href="' + rss_settings['base'] + File.basename(configs.rss_10_file) + '" />')
      end
    end
    if configs.rss_20_file
      createRss('2.0', configs.rss_20_file, rss_settings)
      if configs.rss_insert
        replace_lines.sub!(/<\/title>/, '</title><link rel="alternate" type="application/rss+xml" title="RSS 2.0" href="' + rss_settings['base'] + File.basename(configs.rss_20_file) + '" />')
      end
    end

    # メニューを生成する
    # MENU_BEGIN を検出し、MENU_END までの内容を空文字列で置き換える
    if replace_lines.sub!(/MENU_BEGIN(.+?)MENU_END/, '') then
      menu = $1

      # メニューにフロー用の CSS タグを追加する
      menu = '<div id=wrapper><div id=menu>' + menu + '</div><div id=main>'

      # <body> をメニューの内容と置換する
      replace_lines.sub!(/<body>/, '<body>' + menu)
      replace_lines = replace_lines + '</div>'
    end

    # ファイルの置換
    File.open(page, 'w:utf-8') { |io|
      io.write(replace_lines)
    }
  end
}


# Examples から、共通 PATH を取り除く
# !!! 要、エラー処理。場所の指定がなかった場合について
examples_file = File.dirname(configs.index_page) + '/examples.html'
begin
  lines = File.read(examples_file).force_encoding("utf-8")
rescue => exc
ensure
  lines = ''
end

match_pattern = '">' + configs.strip_path
update = false
if lines.match(match_pattern)
  replace = '">'
  lines.gsub!(match_pattern, replace)
  updated = true
end
# ファイルの更新
if updated
  updatefile(examples_file, lines);
  print 'update "' + examples_file + "\"\n"
end

# *-examples.html から、共通パスを取り除く
# 対象ページの検索を行う
match_pattern = configs.strip_path
unless match_pattern.empty?
  Find.find(File.dirname(configs.index_page)) { |name|
    if name =~ /.+\.html$/
      # 置換処理
      lines = File.read(name).force_encoding("utf-8")
      # !!! 改行を読み飛ばす正規表現に置き換える
      while lines.match(match_pattern)
        lines.sub!(match_pattern, '')
        updated = true
      end

      # ファイルの更新
      if updated
        updatefile(name, lines);
        print 'update "' + name + "\"\n"
      end
    end
  }
end


# doxygen.css に設定を追加する
css_file = File.dirname(configs.index_page) + '/doxygen.css'
File.open(css_file, 'a') { |io|
io.print <<-"EOB"
.history_frame {
  clear:both;
  line-height:19px;
  padding:4px;
  border:solid 2px MediumSlateBlue;
}

.log_frame {
  height:130px;
  overflow:auto;
  border:solid 2px MediumSlateBlue;
  padding:4px;
}

.log_space {
  margin-left: 18px;
}
EOB
}
