#!/usr/bin/ruby

# Copyright (C) 2008, IWAMURO Motonori
# All rights reserved.
#
# License: BSD License (revised)
# see http://vmi.jp/software/ruby/COPYING

### Configuration
MEMO_DIR = '../memo'
FS_CHARSET = :auto
FS_CHARSET_CYGWIN = 'CP932'
HOME_URI = :auto
MEMO_EXT = 'memo'

### Initialize
require 'cgi'
require 'iconv'
require 'time'

BLOCK_SIZE = 64 * 1024
INVALID_UTF8 = /[\xC0\xC1\xF8-\xFD]|\xE0[\x80-\x9F]|\xF0[\x80-\x8F]/n

### Utilities
def comma(num)
  num.to_s.gsub(/(\d{1,3})(?=\d{3}+(?:$|\D))/, '\1,')
end

def round(num)
  (num * 10).round / 10
end

def get_memo_id(title)
  now = Time.now
  now.strftime('%Y%m%d_%H%M%S_') + title + '.' + MEMO_EXT
end

def get_title_by_memo_id(memo_id)
  /^\d+_\d+_(.+)\.#{MEMO_EXT}$/o =~ memo_id
  $1
end

### Views
class Memo < CGI
  HEADER = { 'charset' => 'UTF-8' }
  KBytes = 1024
  MBytes = KBytes * 1024
  GBytes = MBytes * 1024

  def h(s)
    CGI.escapeHTML(s)
  end

  def home_uri
    if HOME_URI != :auto
      HOME_URI
    else
      script_name.chomp('/').sub(/(?:cgi-[^\/]+\/)?[^\/]*$/, '')
    end
  end

  def page_error(status, msg = nil)
    if HTTP_STATUS.has_key?(status)
      status = 'SERVER_ERROR'
    end
    st_msg = HTTP_STATUS[status]
    msg = ' - ' + h(msg) if msg
    out('status' => status) do <<-EOF
<html>
<head>
  <title>#{st_msg}</title>
</head>
<body>
  #{st_msg}#{msg}
</body>
</html>
    EOF
    end
    exit(0)
  end

  def page_edit_memo(memo_id = '', text = '', msg = '')
    if memo_id == ''
      title = 'Tiny Memo'
      write_memo = ''
    else
      memo_title = memo_id.match(/^(\d+_\d+_.+)\.memo$/)[1]
      title = "Edit [#{memo_title}]"
      write_memo = %'<a href="#{script_name}">[Write Memo]</a> |'
      cancel = %'| <a href="#{script_name}/show/#{h memo_id}">[Cancel]</a>'
    end
    out(HEADER) do
      <<-EOF
<html>
<head>
  <title>#{h title}</title>
</head>
<body>
  <h1>#{h title}</h1>
  <hr>
  <p>
  #{write_memo}
  <a href="#{script_name}/list">[List of Memos]</a>
  </p>
  #{h msg}
  <form action="#{script_name}" method="POST">
  <input type="hidden" name="memo_id" value="#{h memo_id}">
  <textarea name="text" cols="80" rows="15">#{h text}</textarea><br>
  <input type="submit" value="Send">
  #{cancel}

  </form>
  <hr>
  <a href="#{home_uri}">[HOME]</a>
</body>
</html>
      EOF
    end
  end

  def page_show_memo(memo_id, text)
    /^\d+_\d+_(.+)\.memo$/ =~ memo_id
    title = $1
    text = h(text).gsub(%r{\b(?:http|https|ftp)://\S+}) do |uri|
      %'<a href="#{uri}">#{uri}</a>'
    end.gsub(/\n/, "<br>\n")
    out(HEADER) do
      <<-EOF
<html>
<head>
  <title>#{h title}</title>
</head>
<body>
  <h1>#{h title}</h1>
  <hr>
  <p>
  #{text}
  </p>
  <hr>
  <form action="#{script_name}/edit" method="POST">
  <input type="hidden" name="memo_id" value="#{memo_id}">
  <input type="submit" value="Edit Memo">
  | <a href="#{script_name}">[Write Memo]</a>
  | <a href="#{script_name}/list">[List of Memos]</a>
  </form>
  <hr>
  <a href="#{home_uri}">[HOME]</a>
</body>
</html>
      EOF
    end
  end

  def file_tr(name, stat)
    title = name.match(/^\d+_\d+_(.+)\.memo$/)[1]
    size = stat.size
    <<-EOF
      <tr>
        <td align="center">
          <input type="checkbox" name="files" value="#{h name}">
        </td>
        <td><a href="#{script_name}/show/#{h name}">#{h title}</a></td>
        <td align="right">#{comma size}</td>
        <td>#{stat.mtime.strftime('%Y-%m-%d %H:%M:%S')}</td>
      </tr>
    EOF
  end

  def page_memo_info(title, list, list_link = false)
    if list.length == 0
      tr = '<tr><td colspan="5" align="center">No Memos.</td></tr>'
    elsif list[0].instance_of?(Array)
      tr = list.map do |name, stat|
        file_tr(name, stat)
      end.join
    else
      tr = file_tr(*list)
    end
    if list_link
      list_of_memos = %'| <a href="#{script_name}/list">[List of Memos]</a>'
    else
      list_of_memos = ''
    end
    out(HEADER) do
      <<-EOF
<html>
<head>
  <title>#{title}</title>
</head>
<body>
  <h1>#{title}</h1>
  <hr>
  <p>
  <a href="#{script_name}">[Write Memo]</a>
  #{list_of_memos}
  </p>
  <form action="#{script_name}/delete" method="POST">
    <table border="1">
      <tr>
        <th>Delete</th>
        <th>Title</th>
        <th>Size</th>
        <th>Date</th>
      </tr>
#{tr}
    </table>
    <br>
    <input type="submit" value="Delete Checked Memo">
  </form>
  <hr>
  <a href="#{home_uri}">[HOME]</a>
</body>
</html>
      EOF
    end
  end
end

# Main
cgi = Memo.new
begin
  Dir.chdir(MEMO_DIR)

  # Detect charset
  if FS_CHARSET != :auto
    $charset = FS_CHARSET
  elsif /\.([^.]+)$/ =~ ENV['LANG']
    $charset = $1
  elsif /cygwin/ =~ RUBY_PLATFORM
    $charset = FS_CHARSET_CYGWIN
  else
    $charset = 'UTF-8'
  end
  if /UTF[-_]?8/i =~ $charset
    def to_fs(name)
      name
    end
    def from_fs(name)
      name
    end
  else
    $to_fs = Iconv.new($charset, 'UTF-8')
    $from_fs = Iconv.new('UTF-8', $charset)
    def to_fs(name)
      $to_fs.iconv(name)
    end
    def from_fs(name)
      $from_fs.iconv(name)
    end
  end

  # Dispatch
  path_info = cgi.path_info.to_s.chomp('/')
  case cgi.request_method + path_info
  when 'GET'
    cgi.page_edit_memo

  when 'GET/list', 'POST/delete'
    if path_info == '/delete'
      cgi.params['files'].each do |name|
        fs_name = to_fs(name)
        begin
          File.delete(fs_name)
        rescue
          # nop
        end
      end
    end
    list = Dir.glob('*.' + MEMO_EXT).select do |fs_name|
      FileTest.file?(fs_name)
    end.sort.reverse.map do |fs_name|
      [from_fs(fs_name), File.stat(fs_name)]
    end
    cgi.page_memo_info('List of Memos', list)

  when %r{^GET/show/(.*)$}
    name = $1
    fs_name = to_fs(name)
    body = IO.read(fs_name)
    cgi.page_show_memo(name, body)

  when 'POST/edit'
    memo_id = cgi['memo_id']
    body = IO.read(to_fs(memo_id))
    cgi.page_edit_memo(memo_id, body)

  when 'POST'
    memo_id = cgi['memo_id'] || ''
    text = cgi['text'] || ''
    if INVALID_UTF8 =~ name
      cgi.page_error('BAD_REQUEST')
    end
    text.sub!(/^\s*\n/m, '')
    text.gsub!(/\s+$/, '')
    if text.length == 0
      cgi.page_edit_memo(memo_id, '', 'メモが入力されていません。')
      exit
    end
    if memo_id == ''
      /^\s*(.*?)\s*$/ =~ text
      title = $1
      title.gsub!(/[\x00-\x20!\"\#%&\'()*+,\/:;<>?@\[\\\]^_`{|}~\x7f]+/, '_')
      title.sub!(/^_/, '')
      title.sub!(/_$/, '')
      memo_id = get_memo_id(title)
    else
      title = get_title_by_memo_id(memo_id)
    end
    fs_name = to_fs(memo_id)
    open(fs_name, 'w') do |ofh|
      ofh.binmode
      ofh.write(text)
    end
    stat = File.stat(fs_name)
    cgi.page_memo_info("Written - " + title, [memo_id, stat], true)

  when /POST|GET/
    cgi.page_error('NOT_FOUND')

  else
    cgi.page_error('NOT_IMPLEMENTED')
  end
rescue => e
  cgi.page_error('SERVER_ERROR', e.to_s)
end
