
# See http://www.bloglines.com/blog/ThomasEEnebo?id=40

# 使い方:
#  $ jruby edinetinstance.rb [XBRL_Instance ...]
#  $ jruby edinetinstance.rb [XBRL_Instance.zip ...]
#  例:
#    $ jruby edinetinstance.rb
#    $ jruby edinetinstance.rb ../../sample2009-03-09/X99005-000/jpfr-asr-X99005-000-2009-03-31-01-2009-06-27.xbrl
#    $ jruby edinetinstance.rb ../../sample2009-edinet/XBRL_20090517_155420.zip
#
# 2009-04-25 katoy 0.1.0
# 
# See CahnbeLog

APP_VERSION = '0.2.10'  # See ChangLog
APP_TITLE = "View EDINET Instance (ver #{APP_VERSION})"

require 'pp'
require 'edinetlabel'
require 'xbrl'
require 'barfactory'
require 'edinetzip'

require 'jruby'
require 'swingx-0.9.7.jar'

import org.jdesktop.swingx.JXTreeTable
import org.jdesktop.swingx.treetable.DefaultMutableTreeTableNode
import org.jdesktop.swingx.treetable.DefaultTreeTableModel
import org.jdesktop.swingx.treetable.AbstractTreeTableModel
import org.jdesktop.swingx.decorator.HighlighterFactory

import javax.swing.JFrame
import javax.swing.BorderFactory
import javax.swing.border.Border
import javax.swing.BoxLayout

import javax.swing.JTabbedPane
import javax.swing.UIManager

import javax.swing.table.DefaultTableCellRenderer
import javax.swing.table.JTableHeader
import javax.swing.table.TableColumn
import javax.swing.table.DefaultTableModel
import javax.swing.table.TableCellRenderer
import javax.swing.table.TableColumnModel

import javax.swing.tree.TreeSelectionModel
import javax.swing.tree.TreePath
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeModel

import java.awt.BorderLayout
import java.awt.Color
import java.awt.Component

import java.awt.datatransfer.DataFlavor
import java.awt.dnd.DnDConstants
import java.awt.dnd.DropTarget
import java.awt.dnd.DropTargetAdapter
import java.awt.dnd.DropTargetListener

MAX_COL = 24  # taxonomy データ csv の列数
USER_TEMPLATE_DIR = '../user_2009'
USER_TEMPLATE_FILE_NAME = 'config.yaml'
FIND_HISTORY_MAX = 10  # search 候補の履歴数
DEFALUT_TREE_WIDTH = 600

class UserTemplate
  @@templates = nil
  def self.get_templates
    @@templates = YAML.load_file(
      "#{USER_TEMPLATE_DIR}/#{USER_TEMPLATE_FILE_NAME}")
  end

  def self.get_filename(kind_sym)
    UserTemplate.new if @@templates == nil
    @@templates.each do |item|
      if item[0] == kind_sym
        return "#{USER_TEMPLATE_DIR}/#{item[1]}"
      end
    end
    pp "--- Not defined #{kind_sym}"
    ''
  end
end

# Document (tree 要素)
class MyDocumentTreeNode < DefaultMutableTreeTableNode
  attr_reader :info

  def initialize(name, instance)
    super(name)
    @info = {}
    instance.each do |k, v|
      k_str = k.to_s
      if !k_str.include?('Context')  and !k_str.include?('ExtendedLinkRole')
        value = v[:DocumentInfo].to_s
        if (value != '')  and (value != 'true') and (value != 'false')
          @info[k] = value
        end
      end
    end
  end

  def include_str?(str)
    @info.each do |k, v|
      return true if k != nil and k.include? str
      return true if v != nil and v.include? str
    end
    return false
  end

  def show_property(model)
    # Document 情報を表示する
    index = 0
    @info.each do |k, v|
      model.setValueAt(k, index, 0)
      model.setValueAt(v, index, 1)
      index += 1
    end
  end

end

# レポート名 (tree 要素)
class MySheetTreeNode < DefaultMutableTreeTableNode
  attr_reader :info

  def initialize(info)
    super(info)
    @info = info
  end

  def include_str?(str)
    @info.each do |v|
      return true if v != nil and v.include? str
    end
    return false
  end

  def show_property(model)
    # レポート名をクリックした時は、何も表示しない
  end

end

# 科目情報 (tree 要素)
class MyItemTreeNode < DefaultMutableTreeTableNode
  attr_reader :info
  attr_reader :titles
  attr_reader :fact

  def initialize(info, titles, fact)
    super(info)
    @info = info
    @titles = titles
    @fact = fact
  end

  def include_str?(str)
    @info.each do |v|
      return true if v != nil and v.include? str
    end
    if fact !=nil
      fact.each do |k,v|
        return true if k.to_s.include? str
        return true if v != nil and v.to_s.include? str
      end
    end
    return false
  end

  def show_property(model)
    # レポート名をクリックした時は、タクソノミ情報を表示する
    # 科目情報の名前
    @titles.each do |key, val|
      model.setValueAt(key, val, 0)
    end
    # 科目情報の値
    @info.each_index do |index|
      model.setValueAt(@info[index], index, 1)
    end
  end

end

class MyTreeTableModel < DefaultTreeTableModel
  attr_reader :report_name
  attr_reader :company_name
  attr_reader :context_size
  attr_reader :context_name
  attr_reader :context_id

  def initialize(node, contexts, report_sym, company_name)
    super(node)

    @report_sym = report_sym
    edinet_info = EdinetTaxonomyInfo::kind2names(report_sym)
    if edinet_info == nil
      @report_name = report_sym.to_s
    else
      @report_name = edinet_info[1]
    end
    @company_name = company_name
    @context_size = contexts.keys.size
    @context_ids = []
    @context_names = []
    contexts.each do |k, v|
      @context_ids << k
      @context_names << v
    end
  end

  def getColumnName(index)
    return "name" if index == 0
    @context_names[index - 1]
  end

  def getColumnCount
    @context_size + 1
  end

  def getValueAt(node, columnIndex)
    if columnIndex == 0
      if node.instance_of? MySheetTreeNode
        return node.info[:name]
      elsif node.instance_of? MyItemTreeNode
        if node.info[15] == '[合計]'
          return node.info[1] + '[合計]'
        else
          return node.info[1]
        end
      else
        return @report_name
      end
    elsif
      if node.instance_of? MySheetTreeNode
        return ''
      elsif node.instance_of? MyItemTreeNode
        if node.fact == nil
          return ''
        end
        c_id = @context_ids[columnIndex - 1]
        v = node.fact[c_id].to_s
        return v if v == ''
        return v.to_i
      else
        return ''
      end
    end
  end

end

class TreeListener
  include javax.swing.event.TreeSelectionListener

  def initialize(table)
    @table = table
  end

  def valueChanged(event)
    path = event.getPath
    show_property(path.getLastPathComponent)
  end

  def show_property(info)
    model = @table.getModel
    # 表示データのクリア
    MAX_COL.times do |i|
      model.setValueAt('', i, 0)
      model.setValueAt('', i, 1)
    end
    # ツリーノードに沿ったデータの表示
    info.show_property(model)
  end
end

def get_edinet_dataall(kind, instance)
  root = MyDocumentTreeNode.new("EDINET-2009-03-09", instance)
  root.add(get_edinet_data(kind, instance))
  root
end

class ColorRenderer
  include TableCellRenderer

  def initialize(anotherRenderer)
    @delegate = anotherRenderer
  end

  def getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column)

    if (value.to_i >= 0)
      fgrd = Color.black
    else
      fgrd = Color.red
    end
    
    if value != nil and value != '' and (/合計/ =~ table.getModel.getValueAt(row, 0))
      border = BorderFactory.createMatteBorder(2,0,0,0,Color.black)
    end

    c = @delegate.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column)
    c.setForeground(fgrd)
    c.setBorder(border) if border != nil
    return c;
  end
end

class MyRenderer < DefaultTableCellRenderer
  def setValue(v)
    setHorizontalAlignment(javax.swing.JLabel::RIGHT)
    s = v.to_s
    setText(s.reverse.gsub( /(\d{3})/, "\\1," ).chomp( "," ).reverse)
  end
end

class MyDropTargetAdapter < DropTargetAdapter
  def initialize(panel)
    super()
    @panel = panel
  end

  def drop(e)
    transfer = e.getTransferable();
    if transfer.isDataFlavorSupported(DataFlavor.javaFileListFlavor)
      # ファイルのパス
      e.acceptDrop(DnDConstants::ACTION_COPY_OR_MOVE)
      file_list = transfer.getTransferData(DataFlavor.javaFileListFlavor)
      file_path = file_list[0].getAbsolutePath()

      if EdinetZip::xbrl_instance?(file_path)
        @panel.change_file(EdinetZip::get_instance_files(file_path)[0])
      end
    end
  end
end

class MyTemplateTreeModel < DefaultTreeModel
  def initialize
    root = DefaultMutableTreeNode.new([:Templates,nil])
    super(root)

    # ユーザー登録テンプレート
    user_templates = DefaultMutableTreeNode.new([:USER, nil])
    root.add(user_templates)
    templates = UserTemplate.get_templates
    templates.each do |item|
      c = DefaultMutableTreeNode.new(MyTemplateTreeNode.new(item[2], item[0]))
      user_templates.add(c)
    end

    # EDINE業界別の標準テンプレート
    edinet_templates = EdinetTaxonomyInfo::get_templates
    edinets = DefaultMutableTreeNode.new([:EDINET, nil])
    root.add(edinets)
    edinet_templates.each do |kc,vc|
      c = DefaultMutableTreeNode.new(MyTemplateTreeNode.new(kc, vc))
      edinets.add(c)
    end

  end
end

class MyTemplateTreeNode
  attr_reader :val
  def initialize(k,v)
    @val = [k,v]
  end

  def to_s
    "#{@val[0].to_s} (#{@val[1].to_s})"
  end
end

class MyPanel < javax.swing.JComponent
  attr_reader :my_pane
  attr_reader :file_path
  attr_reader :kind_sym
  attr_reader :item_tree_view
  attr_accessor :lang

  @@tab_table_pane = nil
  @@tree_listener = nil
  @@tree_width = 0

  def initialize
    super()
    setLayout(BorderLayout.new)
    @lang = 'ja'
  end

  def set_data(file_path, kind_sym = nil, reload = true)
    @file_path = file_path
    @kind_sym = kind_sym

    if file_path != nil
      if reload
        @xbrl = nil
        @tree_model = nil

        @xbrl = Xbrl.new
        @xbrl.parse_instance(file_path)
        @kind_sym = get_kind(@xbrl.kinds).to_sym  if @kind_sym == nil

        BarFactory::update_history(file_path, self)
      end

      contexts = @xbrl.contexts
      instance = @xbrl.get_instance_table(@lang)
      company_name = instance['jpfr-di:EntityNameJaEntityInformation'][:DocumentInfo].to_s
      @tree_model = MyTreeTableModel.new(get_edinet_dataall(@kind_sym, instance), contexts, @kind_sym, company_name)
    end

    setup_panel
  end

  # 分割位置を保持する
  def get_tree_width
    if @my_pane.instance_of? javax.swing.JSplitPane
      @my_pane.getDividerLocation
    else
      DEFALUT_TREE_WIDTH
    end
  end

  def change_file(file_path, kind_sym = nil)
    pp file_path  # ファイル名をエコーする
    save = @my_pane
    set_data(file_path)
    remove(save)
    validate
  end

  def change_template(kind_sym)
    pp kind_sym  # テンプレート名をエコーする
    save = @my_pane
    set_data(@file_path, kind_sym, false) # false: file を読み直さない
    remove(save)
    validate
  end

  def get_kind(kinds)
    ans = 'cte'   # default
    if kinds != nil
      kinds.each do |k|
        ans = k if k != 'cte' and k != 'cai'
      end
    end
    ans
  end

  def setup_panel
    removeAll
    if @tree_model == nil
      # instnce ファイルが未指定の場合、ボタンだけを表示する
      @my_pane = setup_panel_button
    else
      # instance 内容の表示画面
      @my_pane = setup_panel_tree(@tree_model)
    end
    add(@my_pane, BorderLayout::CENTER)

    #--- search box
    controls = javax.swing.JPanel.new
    controls.setLayout(BoxLayout.new(controls, BoxLayout::LINE_AXIS))
    label_search = javax.swing.JLabel.new('Search:')
    combo = javax.swing.JComboBox.new
    combo_model = javax.swing.DefaultComboBoxModel.new
    combo.setModel(combo_model)
    combo.setEditable(true)

    # ENTER キーでも検索実行する
    combo.add_action_listener do |ev|
      self.find_all(combo)
    end

    button_all = javax.swing.JButton.new('Find All')
    button_all.add_action_listener do |ev|
      self.find_all(combo)
    end

    controls.add(label_search)
    controls.add(combo)
    controls.add(button_all)
    controls.setBorder(BorderFactory.createTitledBorder("Search"))

    add(controls, BorderLayout::SOUTH)
  end

  def add_item_to_combo(combo, str)
    return false if (str == nil) or (str.strip == '')

    combo.setVisible(false)
    model = combo.getModel
    model.removeElement(str)
    model.insertElementAt(str, 0)
    model.removeElementAt(FIND_HISTORY_MAX) if model.getSize > FIND_HISTORY_MAX
    combo.setSelectedIndex(0)
    combo.setVisible(true)
    return true
  end

  def find_all(combo)
    str = combo.getEditor().getItem
    if add_item_to_combo(combo, str)
      # データを読み込んでいないなら、検索しない
      return if @item_tree_view == nil

      sm = @item_tree_view.getTreeSelectionModel
      @item_tree_view.clearSelection
      find_all_sub(sm, str, @item_tree_view.getPathForRow(0))
    end
  end

  def find_all_sub(tree, str, path)
    node = path.getLastPathComponent
    return if node == nil
    
    if node.include_str? str
      tree.addSelectionPath(path)
    end

    if ( !node.isLeaf() && node.getChildCount() >= 0)
      e = node.children()
      while e.hasMoreElements()
        find_all_sub(tree, str, path.pathByAddingChild(e.nextElement()))
      end
    end
  end

  def setup_panel_button
    # ボタン
    button = javax.swing.JButton.new('Drop down one XBRL instance file or Zip-file.')
    # drag & drop
    dta = MyDropTargetAdapter.new(self)
    DropTarget.new(button, dta)

    button
  end

  def setup_panel_tree(tree_model)
    my_pane = nil

    if @@tab_table_pane == nil
      # 右フレーム
      # テーブル (タクソノミ情報)
      table_view = javax.swing.JTable.new()
      table_model = DefaultTableModel.new(MAX_COL, 2)
      table_view.setModel(table_model)
      # スクルールペイン
      table_scroll_pane = javax.swing.JScrollPane.new
      table_scroll_pane.setAutoscrolls(true)
      table_scroll_pane.setViewportView(table_view)

      # ツリー(テンプレート一覧)
      template_view = javax.swing.JTree.new
      template_tree_model = MyTemplateTreeModel.new
      template_view.setModel(template_tree_model)
      template_view.add_mouse_listener do |ev|
        if (ev.getID == java.awt.event.MouseEvent::MOUSE_CLICKED) and (ev.getClickCount() == 2)
          path = template_view.getPathForLocation(ev.getX(), ev.getY())
          return if path == nil
          node = template_view.getLastSelectedPathComponent
          return if node == nil
          self.change_template(node.getUserObject.val[1])
        end
      end

      # スクルールペイン
      template_scroll_pane = javax.swing.JScrollPane.new
      template_scroll_pane.setAutoscrolls(true)
      template_scroll_pane.setViewportView(template_view)
    
      @@tab_table_pane = JTabbedPane.new
      @@tab_table_pane.addTab('Taxonomy Info', table_scroll_pane)
      @@tab_table_pane.addTab('Template List', template_scroll_pane)
      @@tree_listener = TreeListener.new(table_view)
    end

    # 左フレーム
    # ツリー (インスタンス情報)
    @item_tree_view = JXTreeTable.new

    r = MyRenderer.new
    @item_tree_view.setDefaultRenderer(java.lang.Object, r)
    @item_tree_view.setTreeTableModel(tree_model)

    custom_cell_renderer = ColorRenderer.new(@item_tree_view.getDefaultRenderer(java.lang.Integer))
    cm = @item_tree_view.getColumnModel()
    1.upto(@item_tree_view.getColumnCount - 1) do |i|
      cm.getColumn(i).setCellRenderer(custom_cell_renderer)
    end

    # スクルールペイン
    tree_scroll_pane = javax.swing.JScrollPane.new
    tree_scroll_pane.setAutoscrolls(true)
    tree_scroll_pane.setViewportView(@item_tree_view)

    tab_tree_pane = JTabbedPane.new
    tab_tree_pane.addTab(tree_model.company_name, tree_scroll_pane)

    @item_tree_view.addTreeSelectionListener(@@tree_listener)

    # drag & drop
    dta = MyDropTargetAdapter.new(self)
    DropTarget.new(@item_tree_view, dta)
    DropTarget.new(table_view, dta)
    
    # ２ペイン(左右)
    @@tree_width = get_tree_width  # 分割位置を保持する
    my_pane = javax.swing.JSplitPane.new(javax.swing.JSplitPane::HORIZONTAL_SPLIT,
      tab_tree_pane, @@tab_table_pane)
    my_pane.setDividerLocation(@@tree_width)
  
    @item_tree_view.addHighlighter(HighlighterFactory::createSimpleStriping(HighlighterFactory::QUICKSILVER))
    @item_tree_view.setColumnControlVisible(true)
    @item_tree_view.expandRow(0)
    @item_tree_view.expandRow(1)
    @item_tree_view.expandRow(2)
    @item_tree_view.packAll
    @item_tree_view.setHorizontalScrollEnabled(true)
    
    my_pane
  end

  def visit_all(tree, parent, expand)
    node = parent.getLastPathComponent
    if !node.isLeaf() and node.getChildCount >= 0
      e = node.children
      while e.hasMoreElements
        n = e.nextElement
        path = parent.pathByAddingChild(n)
        visit_all(tree, path, expand)
      end
    end

    if(expand)
      tree.expandPath(parent)
    else
      tree.collapsePath(parent)
    end
  end

end

def get_edinet_data(kind_sym, instance)

  edinet_info = EdinetTaxonomyInfo::kind2names(kind_sym)
  if edinet_info == nil
    # ユーザー定義テンプレート
    kind_name = kind_sym.to_s
    file_path = UserTemplate::get_filename(kind_sym)
    v = EdinetTaxonomyInfo::load_file(file_path)
  else
    # EDINET テンプレート
    kind_name = edinet_info[1]
    v = EdinetTaxonomyInfo::load(kind_sym)
  end
  root = MyDocumentTreeNode.new(kind_name, instance)

  v.sheets.each { |s|
    paths = []
    unless (s[:name] == nil)
      reportNode = MySheetTreeNode.new(s)
      root.add(reportNode)

      paths = []
      paths[0] = reportNode

      s[:labels].each { |item|
        id = item[13] + ":" + item[14]
        itemNode = MyItemTreeNode.new(item, s[:title], instance[id])
        current_depth = item[DEPTH_INDEX].to_i

        if current_depth > 0
          paths[current_depth] = itemNode
          paths[current_depth - 1].add(itemNode)
        end
      }
    end
  }
  root = purge(root, name, instance)
end

def purge(r, name, instance)
  ans = MyDocumentTreeNode.new(name, instance)
  r.children.each do |c|
    node = MySheetTreeNode.new(c.info)
    ans.add(node)

    child = purge_c(c)
    child.each do |cc|
      node.add(cc)
    end
  end
  ans
end

# fact データが無い列, 子供がすべてデータ無しの行を削除する
def purge_c(src)
  ans = []
  src.children.each do |c|
    if (c.getChildCount > 0) or ( (c.fact != nil) and (c.fact.size > 0) )
      if c.getChildCount == 0
        node = MyItemTreeNode.new(c.info, c.titles, c.fact)
        ans << node
      else
        child = purge_c(c)
        if child.size > 0
          node = MyItemTreeNode.new(c.info, c.titles, c.fact)
          child.each do |cc|
            node.add(cc)
          end
          ans << node
        end
      end
    end
  end
  ans
end

if __FILE__ == $0
  # file_path = '../../sample2009-03-09/X99002-000/jpfr-q1r-X99002-000-2009-06-30-01-2009-08-08.xbrl'
  # file_path = '../../sample2009-edinet/XBRL_20090517_155420.zip '
  # ARGS << file_path

  frame = JFrame.new APP_TITLE
  frame.setDefaultCloseOperation(JFrame::EXIT_ON_CLOSE)
  frame.set_size 800,450
  app_panel = MyPanel.new
  bf = BarFactory.new
  frame.setJMenuBar(bf.create_menubar(app_panel))
  frame.add(bf.create_toolbar(app_panel), BorderLayout::NORTH);
  app_panel.set_data(nil)

  instance_paths = EdinetZip::get_instance_files(ARGV)
  instance_paths.each do |instance|
    app_panel.set_data(instance)
  end

  pane = frame.content_pane
  pane.add(app_panel)
  frame.visible = true
end
