#!/usr/bin/perl -w
#
# gnview 2ch BBS Viewer
# http://gnview.sourceforge.jp/

#--------------------------------------
# ライブラリ読み込み
#--------------------------------------

use utf8;
use strict;
use Config;
BEGIN {
   $Config{useithreads} or die "Recompile perl with ithreads to use this program.";
}


use threads;
use threads::shared;
use Thread::Queue;
#--------------------------------------
# スレッド間共有変数
#--------------------------------------
our $gnDebugFlag : shared = 0;	# デバッグレベル指定(0 = まったく出力しない, 1-->大きくなるほど詳細な
                                        #                    デバッグ出力が行われる)
our $hold : shared;
our $gnImgGetSt : shared;
our $queue : shared = Thread::Queue->new;
our $fnsize : shared;

use POSIX qw/ locale_h strftime /;
#use Switch;
use Encode qw/ encode from_to decode /;
use Encode::Guess qw/ euc-jp shiftjis /;
use Time::HiRes qw/ sleep gettimeofday tv_interval /;

use Time::Local;

use Gtk2 '-init';	# perl-Gtk2 パッケージが必要
use Gtk2::GladeXML;	# perl-Gtk2-GladeXML パッケージが必要
use Gtk2::Pango;
use Glib::Object::Subclass
    Glib::Object::,
    interfaces => [ Gtk2::TreeModel::, Gtk2::TreeSortable:: ],
    ;


use HTML::TokeParser;
use HTML::Entities;
use HTTP::Date;
use Compress::Zlib;	# perl-Compress-Zlibパッケージが必要
use List::Util qw/ min /;

use LWP::UserAgent;	# perl-libwww-perl パッケージが必要
use LWP::MediaTypes qw/ guess_media_type /;
use HTTP::Cookies;

#use Image::Magick;	# ImageMagick-perl パッケージが必要

use Data::Dumper;

#--------------------------------------
# 全体設定
#--------------------------------------

use constant TRUE  => 1;
use constant FALSE => 0;

our $gnIdxFileVersion = "1.01";	# よくわかんないけどFolder.idxのなかに出てくるので入れるようにした
					# ギコナビ互換用
# gnviewの名前
our $gnGnname = "gnview";
# gnviewのバージョン
our $gnVersion = "0.7";
   
# ユーザーエージェント指定
our $gnUsrAgent : shared = "Monazilla/1.00 \(${gnGnname}\/${gnVersion}\)";

#--------------------------------------
# グローバル変数
#--------------------------------------

our $gnEnvFile;
our $gnURLIniFile;
our $gnSentIniFile;

our %gnEnvArg;		# gikoNavi.ini保存配列
our %gnEnvArgN;

our %gnURLIni;		# url.iniの内容をハッシュ配列に格納
our %gnInitCfg;	# 初期設定保存配列

our $gnCacheTblFile;


our $boardcfg;				# 板一覧設定ファイル
our %gnBoardTbl;

our $gnIconfn = &gnGetLibPath . "\/${gnGnname}.png";;	# アイコン
#our $gnIconPixbuf = Gtk2::Gdk::Pixbuf->new();

our $logdir;
our $gladefn = &gnGetLibPath . "\/${gnGnname}.glade";   # GladeでデザインしたGUIのXMLファイルの場所
our $gnEnvABimgfn = &gnGetLibPath . "\/1pix.png";
our $gnEnvABcenterimgfn = &gnGetLibPath . "\/1pix_glay.png";

my $gnPrg_firstrun = &gnGetLibPath . "\/${gnGnname}_firstrun.pl";
my $gnPrg_env = &gnGetLibPath . "\/${gnGnname}_env.pl";

our $gnHoverOverLink = FALSE;            # カーソルがリンクの上にあるかのフラグ
our $gnPopupText     = "";               # レス表示ウィンドウがポップアップしているかのフラグ
                                        # ポップアップがなければNULL,
                                        # ポップアップしていればGtk2::Windowが入っている
our $gnPopupImg      = undef;            # 画像表示ウィンドウがポップアップしているかのフラグ
                                        # ポップアップがなければundef,
our %gnImgPopup_info;			# ポップアップ画像の情報を格納する配列
our $gnPop2;
                                        # ポップアップしていればGtk2::Windowが入っている
our $gnPopupImgBuf;			# ポップアップ用画像バッファ
our $gnTmpfn;
our %gnCacheTbl;	# 画像キャッシュテーブル保存配列

#--------------------------------------
# グローバル定数
#--------------------------------------
   
# カーソル作成
# スレのポップアップメニュー用
our $gnCursor_Beam = Gtk2::Gdk::Cursor->new('xterm');
our $gnCursor_Link = Gtk2::Gdk::Cursor->new('hand2');

# 2ch用定数
our $gnKeyTime = "1104688508";

#--------------------------------------
# グローバル変数
#--------------------------------------
our $gnItaLogDir = '';                   # 板毎のログ格納ディレクトリ
our @gnSureViewInfo;                     # スレ表示管理用配列
                                        # n(偶数,0スタート): スレファイル名
our $gnSureViewPageNum = 0;		# スレ表示用ページの最終番号
                                        # n+1(奇数,1スタート): Notebookタブの番号
our $tmpdir = &gnGetTmpDir;		# テンポラリディレクトリを取得
if($gnDebugFlag) { print "$tmpdir\n"; }

our $gnTimeforPopup = 0;		# ポップアップウィンドウ遅延表示用
our $gnPopupCallback;
our $gnImgUpdateCallback;
our $gnPopupCnt = 0;

our $gnSuppressEventFlg = FALSE;	# コールバック関数を実行するかどうかのフラグ。
                                    # TRUEならコールバック時に何もしない
our $gnSignalHandler;			# コールバック関数

our $gnGUIxml;				# gladeで生成したXMLファイル格納用
our $gnFRxml;				# gladeで生成したXMLファイル格納用
our $gnWxml = "";				# gladeで生成したXMLファイル格納用
our $gnIPxml;
our $gnISxml;
our $gnEnvXML;
our $gnEnvABXML;

#--------------------------------------
# 初期処理
#--------------------------------------
require($gnPrg_firstrun);
require($gnPrg_env);

print STDERR "Starting $gnGnname $gnVersion\n";
  #____________________________________________________
  # 初回起動かどうかをチェック
  our $gnCfgPath = &gnGetPersonalCfgPath;
  my $gnCfgFileN = $gnCfgPath . "\." . "${gnGnname}config";
  if($gnDebugFlag) { print Dumper($gnCfgFileN); }
  if (!(-e $gnCfgFileN)) {
     &gnFirstRun($gladefn);
  }else{
     &gnInit;
  }
  #--------------------------------------
  # スレッドの開始
  my $thr = threads->new (\&command_runner);   #<--1000usec程度かかっている

  Gtk2->main;


#--------------------------------------
# 初回起動処理
#--------------------------------------

sub gnInit {
  
  # 初期設定ファイル(.gnconfig)読み込み
  print STDERR "Loading Configration File\n";
  my $gnCfgPath = &gnGetPersonalCfgPath;
  my $gnCfgFileN = $gnCfgPath . "\." . "${gnGnname}config";
  if (!(-e $gnCfgFileN)) { &gnRestartInit; }
  my $gnInitCfgRef = &gnLoadEnv($gnCfgFileN);
  %gnInitCfg = %$gnInitCfgRef;
  if($gnDebugFlag) { print "gnInit_LoadCfg=\n" . Dumper(%gnInitCfg); }
  $gnCfgPath = $gnInitCfg{'Main'}{'gnCfgPath'};
  
  $gnEnvFile = $gnInitCfg{'Main'}{'gikoNavi.ini'};
  if (!(-e $gnEnvFile)) { &gnRestartInit; }
  $gnURLIniFile = $gnInitCfg{'Main'}{'url.ini'};
  if (!(-e $gnURLIniFile)) { &gnRestartInit; }
  $gnSentIniFile = $gnInitCfg{'Main'}{'sent.ini'};
  if (!(-e $gnSentIniFile)) { &gnRestartInit; }
  $gnCacheTblFile = $gnInitCfg{'Main'}{'cachetbl.ini'};
  if($gnDebugFlag) { print "cachetbl.ini\n".Dumper($gnCacheTblFile); }
  if ($gnCacheTblFile eq "") { &gnRestartInit; }
  $logdir = $gnInitCfg{'Main'}{'LogFolder'} . "\/2ch\/";
  if (!(-e $logdir)) { &gnRestartInit; }

  
  $boardcfg = $gnCfgPath . "2chboard.lst";
  $gladefn = &gnGetLibPath . $gnGnname . ".glade";
  
  # 環境設定ファイル読み込み(gikoNavi.ini)
  if($gnDebugFlag) { print "gikonaviini=\n" . Dumper($gnEnvFile); }
  my $gnEnvArgRef = &gnLoadEnv($gnEnvFile);
  %gnEnvArg = %$gnEnvArgRef;
  $gnEnvArg{'Folder'}{'LogFolderUnix'} = $gnInitCfg{'Main'}{'LogFolder'};
  %gnEnvArgN = %gnEnvArg;
  
  # 環境設定ファイル読み込み(url.ini)
  if($gnDebugFlag) { print "urliini=\n" . Dumper($gnURLIniFile); }
  my $gnUrlArgRef = &gnLoadEnv($gnURLIniFile);
  %gnURLIni = %$gnUrlArgRef;
  if($gnDebugFlag) { print "urlini=\n" . Dumper(%gnURLIni); }

  # 環境設定ファイル読み込み(cachetbl.ini)
  if($gnDebugFlag) { print "cachetbl.ini=\n" . Dumper($gnCacheTblFile); }
  my $gnCacheTblRef = &gnLoadEnv($gnCacheTblFile);
  %gnCacheTbl = %$gnCacheTblRef;

  # 起動時に環境を読み込みメインウィンドウを開く
  $gnGUIxml = Gtk2::GladeXML->new($gladefn, 'mW');
  $gnGUIxml->signal_autoconnect_from_package('main');


  #____________________________________________________
  # 板一覧読み込み
  print STDERR "Building Board List...";
  # 板一覧のファイル名を与えると、TreeStore型のウィジェットが帰ってくる
  my $gnItaStore;
  if (-e $boardcfg) {
     $gnItaStore = &gnMakeItaTreeStore($boardcfg);
  }else{
     &gnWarn("板一覧を保存するファイルが見つかりません\n板更新ボタンを押して板の一覧を取得してください");
  }
  print STDERR "...Complete";
  
  # TreeView作成
  my $gnItaView = $gnGUIxml->get_widget('gnII');
  $gnItaView->set_model($gnItaStore);
  
  my $gnIICol1 = Gtk2::TreeViewColumn->new();
#  $gnIICol1->set_title(Encode::decode('utf8','２ちゃんねる'));
  $gnIICol1->set_title('２ちゃんねる');
  
  my $gnIICol1Rend = Gtk2::CellRendererText->new;
  $gnIICol1->pack_start($gnIICol1Rend, FALSE);
  $gnIICol1->add_attribute($gnIICol1Rend, text => 0);

  $gnItaView->append_column($gnIICol1);
  
  my $gnIS_pos = Gtk2::TreePath->new_from_indices(0);
  $gnItaView->expand_row($gnIS_pos, FALSE);
  
  
  # 板名が選択されたらスレ一覧読み込み
  #my @gnForceFLG = ("none");
  $gnItaView->signal_connect('cursor-changed' => sub{ &gnSleIchiranSelect($gnItaView,"none"); });
  
  #____________________________________________________
  # スレ一覧初期設定
  #
  my $gnSureLstStore = &gnMakeInitialSureLst;
  &gnSureIchiranInit($gnSureLstStore, undef);
  
  #____________________________________________________
  # スレ表示部初期設定
  my $gnNoteInit = $gnGUIxml->get_widget('gnNote');
  $gnNoteInit->set_property(homogeneous => FALSE);
  $gnSignalHandler->{gnNote}{switch_page} = $gnNoteInit->signal_connect_after('switch-page' => sub{ &on_gnNote_switch_page; });
  if($gnDebugFlag) { print Dumper($gnSignalHandler)."\n"; }

  #--------------------------------------
  # メインウィンドウ表示
  #--------------------------------------

   # iniでサイズが指定されている部分はそれに合わせてリサイズする
   my $mW = $gnGUIxml->get_widget('mW');

   # アイコンをセット
   $mW->set_default_icon(&gnGetgnIconPixbuf());
   $mW->set_icon(&gnGetgnIconPixbuf());

   # メインウィンドウのリサイズ
   $mW->resize($gnEnvArg{'WindowSize'}{'Width'}, $gnEnvArg{'WindowSize'}{'Height'});
   $mW->move($gnEnvArg{'WindowSize'}{'Left'}, $gnEnvArg{'WindowSize'}{'Top'});
   $mW->queue_resize;
   
   # カテゴリ一覧の幅を環境変数から読み取ってセット
   my $gnCatHPan = $gnGUIxml->get_widget('gnCatHPan');
   $gnCatHPan->set_position($gnEnvArg{'CategoryColumnWidth'}{'ID0'});

   # 板一覧の高さを環境変数から読み取ってセット
   my $gnItaVPan = $gnGUIxml->get_widget('gnItaVPan');
   $gnItaVPan->set_position($gnEnvArg{'BoardColumnWidth'}{'ID7'});
   


   # メインウィンドウを表示
   $mW->show_all;
   
   print "done. Ready.\n";

}

sub gnRestartInit {
   # 初期設定内容に不備があった場合に、再度初期設定を行うよう
   # 促す関数
   #
   # 引数: なし
   # 返り値: なし
   $gnGUIxml = Gtk2::GladeXML->new($gladefn, 'mW');
   my $mW = $gnGUIxml->get_widget('mW');
   &gnWarn("初期設定内容に不備があるか、\nバージョンの変更に伴い内容に不足が生じました。\n$gnInitCfg{'Main'}{'gnCfgPath'}\.${gnGnname}configを削除するかリネームしてから${gnGnname}をもう一度起動して\n再度初期設定を行ってください");
   exit(1);
}

sub gnGetgnIconPixbuf {
   # gnviewのアイコンイメージを取得する関数
   #
   # 引数: なし
   # 返り値: Gtk2::Gdk::Pixbuf(アイコンイメージ)
   my $gnIconTheme = Gtk2::IconTheme->get_default;
   my $gnIconPixbuf;
   eval{ $gnIconPixbuf = $gnIconTheme->load_icon($gnGnname, 48, 'no-svg'); };
   if ($gnDebugFlag) { print "icon-info\: " . Dumper($gnIconPixbuf); }
   if($@) {
      eval{$gnIconPixbuf = Gtk2::Gdk::Pixbuf->new_from_file($gnIconfn); };
      if($@) {
         $gnIconPixbuf = $gnIconTheme->load_icon("file-broken", 48, 'no-svg');
      }else{
         if ($gnDebugFlag) { print "icon is set from $gnIconfn\n"; }
      }
   }else{
      if ($gnDebugFlag) { print "icon is set from User\'s theme icons\n"; }
   }
         return($gnIconPixbuf);
}

sub gnGetLibPath {
   # ライブラリパスをチェック
   #
   # 引数:      なし
   # 返り値: 1: ライブラリへのフルパス
   
   my $gnLibPath;
   if ($^O eq "MSWin32") {
      # "perl -V"の"osname"がMSWin32の場合は
      # 実行環境がWindowsであると判断
      $gnLibPath = "$ENV{'APPDATA'}" . "\\${gnGnname}\\";
   }else{
      $gnLibPath = "\/usr\/share\/${gnGnname}\/";
   }
      return($gnLibPath);
   
}

sub gnGetTmpDir {
   # テンポラリディレクトリ取得用関数
   # Windowsでは環境変数から取得。それ以外(Linux/*NIXを想定)では決め打ちで/tmpを使用
   # 引数：なし
   # 返り値：文字列(ディレクトリのフルパス)
   
   if($^O eq 'MSWin32') {
      # "perl -V"の"osname"がMSWin32の場合は
      # 実行環境がWindowsであると判断し、環境変数TEMPからテンポラリディレクトリを取得
      # Windows以外(Linuxを想定)の場合は/tmpを決め打ち
      return($ENV{'TEMP'});
   }else{
      return("/tmp");
   }
   
}


sub command_runner {
    ## 別スレッドで動く関数
    # goggify.pl(http://spr.mahonri5.net/oggify/)より使用
    if ($gnDebugFlag) { print STDERR "thread-start\n"; }
    my $run = 1;
    my $run2 = 1; 
    my $gnUA_res;
    my $gnUA;
    my $job;
    my $url;
    my $fn;
    my $proxy_flg;
    my $proxy_addr;
    my $proxy_port;
    my $proxy_uname;
    my $proxy_pass;
    # run until return
    while ($run) {

        $job = $queue->dequeue;
        if ($job eq "geturl") {
           $url = $queue->dequeue_nb;
           $fn = $queue->dequeue_nb;
           $proxy_flg = $queue->dequeue;
              if ($gnDebugFlag>1) { print STDERR "proxyflg\: " . $proxy_flg . "\n"; }
           $proxy_addr = $queue->dequeue_nb;
              if ($gnDebugFlag>1) { print STDERR "proxyaddr\: " . $proxy_addr . "\n"; }
           $proxy_port = $queue->dequeue;
              if ($gnDebugFlag>1) { print STDERR "proxyport\: " . $proxy_port . "\n"; }
           $proxy_uname = $queue->dequeue;
              if ($gnDebugFlag>1) { print STDERR "proxyUserID\: " . $proxy_uname . "\n"; }
           $proxy_pass = $queue->dequeue;
              if ($gnDebugFlag>1) { print STDERR "proxyPass\: " . $proxy_pass . "\n"; }
           {
              lock $gnImgGetSt;
              $gnImgGetSt = TRUE;
           }

           if ($run2 == 1) {
              { lock $fnsize; $fnsize = 0; }
              $gnUA = LWP::UserAgent->new;
              $gnUA->agent($gnUsrAgent);
              $gnUA->timeout(15); # 15秒間無反応だったら中断する
              $run2 = 0;
           }

           # プロキシの設定
           if($proxy_flg == 1) {
               my $gnUA_proxystr = "http\:\/\/" . $proxy_addr . "\:" . $proxy_port . "\/";
               $gnUA->proxy('http', $gnUA_proxystr);
           }else{
               $gnUA->proxy('http', undef);
           }
           if ($gnDebugFlag>1) { print STDERR "proxy\: " . $gnUA->proxy('http') . "\n"; }
           
           my $gnTotSz = 0;
           $gnUA_res = $gnUA->request(HTTP::Request->new(GET => $url),
                                            sub {
                                               my($chunk, $res) = @_;
                                               open(gnFN, ">>$fn");
                                                  binmode gnFN;
                                                  syswrite gnFN, $chunk, length($chunk);
                                               close(gnFN);
                                               $gnTotSz += length($chunk);
                                               if ($gnDebugFlag>1) { print STDERR "thread-write\: " . $gnTotSz . "\/$fnsize bytes\n"; }
                                            });
           {
              lock $gnImgGetSt;
              $gnImgGetSt = FALSE;
           }
        }elsif($job eq "exit") {
           $run = 0;
        }
        { lock $hold; $hold = 0; } 
    }    
    
}

