#/*
# *  Copyright 2007 hkrn <hikarin@users.sourceforge.jp>
# *
# *  Licensed under the Apache License, Version 2.0 (the "License");
# *  you may not use this file except in compliance with the License.
# *  You may obtain a copy of the License at
# *
# *      http://www.apache.org/licenses/LICENSE-2.0
# *
# *  Unless required by applicable law or agreed to in writing, software
# *  distributed under the License is distributed on an "AS IS" BASIS,
# *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# *  See the License for the specific language governing permissions and
# *  limitations under the License.
# */
#
# $Id: Plugin.pm 1247 2007-10-02 14:11:22Z hikarin $
#

package Img0ch::Plugin;

use strict;

sub new {
    my ( $iClass, $iBBS, $key ) = @_;
    my $iKernel       = $iBBS->get_kernel();
    my $repos_data    = $iKernel->get_repos_path('plugin_data');
    my $repos_entries = $iKernel->get_repos_path('plugin_entries');

    bless {
        _bbs           => $iBBS->get_id(),
        __constructors => {},
        __error        => undef,
        _data          => $iKernel->get_repos($repos_data),
        __i0_bbs       => $iBBS,
        __instances    => {},
        _key           => $key,
        _repos         => $iKernel->get_repos($repos_entries),
        _type2num      => {
            'bbs.init'        => 1,
            'mobile.init'     => 2,
            'read.init'       => 3,
            'bbs.filter'      => 4,
            'bbs.upload.pre'  => 5,
            'bbs.upload.post' => 6,
            'bbs.write.pre'   => 7,
            'bbs.write.post'  => 8,
            'bbs.rewrite'     => 9,
            'mobile.rewrite'  => 10,
            'read.rewrite'    => 11,
        },
    }, $iClass;
}

sub load {
    my ( $iPlugin, $from, $called, $iBBS, $key ) = @_;
    my $bbs;

    $from or return [];
    if ($iBBS) {
        $bbs = $iBBS->get_id();
    }
    else {
        $iBBS = $iPlugin->{__i0_bbs};
        $bbs  = $iPlugin->{_bbs};
    }
    $key ||= $iPlugin->{_key} || 0;

    my $iRepos               = $iPlugin->{_repos};
    my $plugin_by_type       = $iRepos->get("I:P.type.${from}") || '';
    my $enabled_global       = $iRepos->get('I:P.enabled') || '';
    my $enabled_local        = $iRepos->get("I:P.enabled.${bbs}") || '';
    my $duplicate_check_hash = {};
    map { $duplicate_check_hash->{$_}++ }
        ( split( ',', $enabled_global ), split( ',', $plugin_by_type ), );
    my $enabled_plugins = {};
    map { $enabled_plugins->{$_} = 1 }
        grep { $duplicate_check_hash->{$_} >= 2 }
        keys %{$duplicate_check_hash};
    %{$duplicate_check_hash} = ();
    map { $duplicate_check_hash->{$_}++ }
        ( split( ',', $enabled_local ), split( ',', $plugin_by_type ), );
    map { $enabled_plugins->{$_} = 1 }
        grep { $duplicate_check_hash->{$_} >= 2 }
        keys %{$duplicate_check_hash};

    my $instance_cache = $iPlugin->{__instances};
    my $constructors   = $iPlugin->{__constructors};
    my $stack          = [];
    my $storage        = $iPlugin->{_storage};
    for my $id ( keys %{$enabled_plugins} ) {
        my $class = $iRepos->get("I:P:E.${id}.class");
        my $meth  = $iRepos->get("I:P:E.${id}.method");
        defined &{"${class}::${meth}"}
            or Img0ch::Kernel->load_module($class);

        my $method_ref = UNIVERSAL::can( $class, $meth ) || next;
        my $constructor = $constructors->{$class}
            || UNIVERSAL::can( $class, 'new' );
        if ( my $instance = $instance_cache->{$class} ) {
            push @{$stack},
                [ $method_ref, $instance, $id, "${class}.${meth}" ];
        }
        elsif ( $constructor and !$instance_cache->{$class} ) {
            my $instance = $constructor->( $class, $iBBS, $key, $called );
            $constructors->{$class}   = $constructor;
            $instance_cache->{$class} = $instance;
            push @{$stack},
                [ $method_ref, $instance, $id, "${class}.${meth}" ];
        }
        else {
            push @{$stack}, [ $method_ref, undef, $id, "${class}/${meth}" ];
        }
    }
    $stack;
}

sub do {
    my ( $iPlugin, $from, $dto, $args ) = @_;
    $args ||= [];
    for my $plugin ( sort { $a->[3] cmp $b->[3] }
        @{ $iPlugin->load( $from, 'do' ) } )
    {
        $plugin->[0]->( ( $plugin->[1] || $dto ), $plugin->[2], @{$args} )
            or return 0;
    }
    1;
}

sub iterator {
    my ( $iPlugin, $from, $fallback ) = @_;
    my $plugin = $iPlugin->load( $from, 'iterator' );
    if ( @{$plugin} ) {
        @{$plugin} = sort { $a->[3] cmp $b->[3] } @{$plugin};
        return sub {
            map { $_->[0]->( $_->[1], @_ ) } @{$plugin};
            }
    }
    else { return $fallback }
}

sub retrive {
    my ( $iPlugin, $id, $bbs, $key, $iKernel ) = @_;
    my $prototype = ref $iPlugin;
    my $iRepos;
    ( defined $bbs or !$prototype ) or $bbs = $iPlugin->{_bbs};

    $bbs ||= 0;
    $key ||= 0;

    if ($iKernel) {
        $iRepos
            = $iKernel->get_repos( $iKernel->get_repos_path('plugin_data') );
    }
    else {
        $iRepos = $iPlugin->{_data};
    }

    my $data = $iRepos->get_binary("I:P:S.${id}.${bbs}.${key}")
        || $iRepos->get_binary("I:P:S.${id}..${key}");
    if ( defined $Storable::VERSION or eval 'use Storable qw(); 1' ) {
        return $data ? Storable::thaw($data)->[2] : undef;
    }
    else {
        defined $FreezeThaw::VERSION or require FreezeThaw;
        return $data ? FreezeThaw::thaw($data)->[2] : undef;
    }
}

sub store {
    my ( $iPlugin, $id, $bbs, $key, $data, $iKernel ) = @_;
    my ( $iReposData, $iReposEntries );
    my $prototype = ref $iPlugin;
    ( defined $bbs or !$prototype ) or $bbs = $iPlugin->{_bbs};

    $bbs ||= 0;
    $key ||= 0;

    if ($iKernel) {
        $iReposData
            = $iKernel->get_repos( $iKernel->get_repos_path('plugin_data') );
        $iReposEntries = $iKernel->get_repos(
            $iKernel->get_repos_path('plugin_entries') );
    }
    else {
        $iReposData    = $iPlugin->{_data};
        $iReposEntries = $iPlugin->{_repos};
    }

    my $class  = $iReposEntries->get("I:P:E.${id}.class")  || return 0;
    my $method = $iReposEntries->get("I:P:E.${id}.method") || return 0;
    if ( defined $Storable::VERSION or eval 'use Storable qw(); 1' ) {
        $iReposData->set_binary( "I:P:S.${id}.${bbs}.${key}",
            Storable::nfreeze( [ $class, $method, $data ] ) );
    }
    else {
        defined $FreezeThaw::VERSION or require FreezeThaw;
        $iReposData->set_binary( "I:P:S.${id}.${bbs}.${key}",
            FreezeThaw::freeze( [ $class, $method, $data ] ) );
    }
    $iReposData->save();
    return 1;
}

1;
__END__

=head1 NAME

Img0ch::Plugin - 各種プラグインを管理するクラス

=head1 SYNOPSYS

  use Img0ch::Plugin

  my $iPlugin = Img0ch::Plugin->new($iBBS, $key);
  my $all_plugin_of_bbs_init = $iPlugin->load('bbs.init');
  # or
  my $all_plugin_of_bbs_init =
      Img0ch::Plugin->load('bbs.init', $iBBS, $key);

  # loading plugins and execute
  $iPlugin->do('bbs.init', $iBBS, \@argument);
  my $all_plugin_of_iterator =
      $iPlugin->iterator('read.rewrite', sub{});

  # store and retrive plugin data
  $iPlugin->store($plugin_id, $bbs, $key, { 'foo' => 'bar' });
  my $hash = $iPlugin->retrive($plugin_id, $bbs, $key);
  # $hash is { 'foo' => 'bar' }

  # alternative of above
  Img0ch::Plugin->store(
      $plugin_id, $bbs, $key,
      { 'foo' => 'bar' }, $iKernel
  );
  my $hash = Img0ch::Plugin->retrive(
      $plugin_id, $bbs, $key
  );
  # $hash is { 'foo' => 'bar' }

=head1 DESCRIPTION

掲示板単体のプラグイン設定情報を1つのオブジェクトとするクラスです。

=head2 new

=over 4

=item Arguments

$iBBS (Img0ch::BBS)

=item Return Value

$iPlugin (Img0ch::Plugin itself)

=back

Img0ch::Pluginのオブジェクトを作成します。

=head2 load

=over 4

=item Arguments

$from, $iBBS?, $key?

=item Return Value

$hash_reference_of_plugin

=back

指定されたI<$from>からプラグインを読み込み、プラグインの情報を格納した
配列のリファレンスを返します。通常この関数は直接呼ばれることはなく、
do()を介して呼ばれます。
クラスとして呼び出すときはI<$iBBS>とI<$key>を指定する必要があります。

I<$from>で指定できる値は以下の通りです。

=over 4

=item bbs.init

bbs.cgiの初期化時に呼び出されます。

=item mobile.init

r.cgiの初期化時に呼び出されます。

=item read.init

read.cgiの初期化時に呼び出されます。

=item bbs.filter

bbs.cgiのフィルタリング時に呼び出されます。

=item bbs.upload.pre

bbs.cgiのファイルアップロード前に呼ばれます。
(ファイルアップロードがあったときのみ)

=item bbs.upload.post

bbs.cgiのファイルアップロード後に呼ばれます。
(ファイルアップロードがあったときのみ)

=item bbs.write.pre

bbs.cgiのdatへの書き込み前に呼び出されます。

=item bbs.write.post

bbs.cgiのdatへの書き込み後に呼び出されます。

=item bbs.rewrite

index.htmlの生成時に1レスごとに呼び出されます。

=item mobile.rewrite

r.cgiのHTML生成時に1レスごとに呼び出されます。

=item read.rewrite

read.cgiのHTML生成時に1レスごとに呼び出されます。

=back

=head2 do

=over 4

=item Arguments

$from, $dto, $argument?

=item Return Value

1 or 0

=back

指定されたI<$from>のプラグインを読み込み、実行します。
I<$dto>にはコンストラクタを持たないプラグインに対して
プラグインのオブジェクトの代わりに渡すオブジェクトを指定します。
配列のリファレンスI<$argument>はその後に渡す引数を指定します。

=head2 iterator

=over 4

=item Arguments

$from, $fallback

=item Return Value

$closure_of_executing_all_plugin

=back

指定されたI<$from>のプラグインを読み込み、それらを実行するクロージャを返します。
I<$fallback>にはプラグインが存在しない時に代わりに生成する関数を指定します。
プラグインが存在しないときはI<$fallback>が返されます。

=head2 store

=over 4

=item Arguments

$plugin_id, $bbs_id, $key, $data, $iKernel?

=item Return Value

1

=back

指定されたI<$plugin_id>のデータを格納します。
I<$bbs_id>とI<$key>を指定することによって特定の掲示板、
特定のスレッドのみプラグインのデータを格納することが出来ますが、
特に指定しない場合はI<$bbs_id>とI<$key>にI<undef>を指定します。
クラスとして呼び出すときはI<$iKernel>を指定する必要があります。

=head2 retrive

=over 4

=item Arguments

$plugin_id, $bbs_id, $key, $iKernel?

=item Return Value

$hash_reference_of_plugin_data

=back

I<$plugin_id>、I<$bbs_id>、I<$key>から格納されたプラグインのデータを取り出します。
データが存在しないときはI<undef>を返します。

=head1 AUTHOR

hkrn E<lt>hikarin@users.sourceforge.jpE<gt>

=cut
