#/*
# *  Copyright 2007-2009 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: Read.pm 1826 2009-04-08 15:23:25Z hikarin $
#

package Img0ch::App::Read;

use strict;
use Img0ch::BBS qw();
use Img0ch::Template qw();
use Img0ch::Thread qw();

BEGIN {
    my $pkg = __PACKAGE__;
    for my $at (qw(read error)) {
        my $attr = '__template_tag_' . $at;
        no strict 'refs';
        *{"${pkg}::add_${at}_tag"} = sub {
            my ( $iApp, $tagset ) = @_;
            ( ref $tagset || '' ) eq 'HASH' or return;
            push @{ $iApp->{$attr} }, $tagset;
            return;
            }
    }
}

sub new {
    my ( $iClass, $iKernel, @args ) = @_;
    bless {
        __agent                 => 0,
        _bbs                    => undef,
        _config                 => $iKernel->get_config(),
        __error                 => '',
        _extend_params          => {},
        _kernel                 => $iKernel,
        __key                   => 0,
        __now                   => 0,
        _param_bbs              => '',
        _param_key              => 0,
        _param_ls               => 0,
        _param_nofirst          => 0,
        _param_single           => 0,
        _param_raw              => '',
        _plugin                 => undef,
        __range_read            => [],
        __read_from             => 0,
        __read_to               => 0,
        _request                => Img0ch::Request->new(@args),
        _setting                => undef,
        __template_tag_error    => [],
        __template_tag_template => [],
        _thread                 => undef,
        __thread_count          => 0,
    }, $iClass;
}

sub run {
    my ($iApp) = @_;
    if ($iApp->init(
            {   disable => 'DisableReadInitPlugin',
                type    => 'read.init',
                mobile  => 0,
            }
        )
        )
    {
        $iApp->load_thread(
            {},
            {   filter   => undef,
                mobile   => 0,
                template => 'read',
                type     => 'read.rewrite',
            },
        );
    }
    else {
        $iApp->redirect_error();
    }
}

sub bbs { $_[0]->{_bbs} }

sub kernel { $_[0]->{_kernel} }

sub request { $_[0]->{_request} }

sub setting { $_[0]->{_setting} }

sub thread { $_[0]->{_thread} }

sub get_param {
    my ( $iApp, $key ) = @_;
    return
           $iApp->{_extend_params}->{$key}
        || $iApp->{_request}->param($key)
        || '';
}

sub get_param_int { Img0ch::Kernel::intval( shift->get_param(@_) ) }

sub load_thread {
    my ( $iApp, $template_vars, $option, $no_disk ) = @_;
    my $key      = $iApp->{_param_key};
    my $iBBS     = $iApp->{_bbs};
    my $iConfig  = $iApp->{_config};
    my $iRequest = $iApp->{_request};
    my $iSetting = $iApp->{_setting};
    my $iThread  = $iApp->{_thread};

    my $mobile = $option->{mobile};
    if ( !$mobile and $iConfig->get('EnableXHTMLBreak') ) {
        $iThread->enable_xhtml_break();
    }

    my $iPlugin = $iApp->{_plugin} || $iBBS->get_plugin_instance($key);
    my $plugin = $iPlugin->iterator( $option->{type}, sub { ${ $_[3] } } );
    my $read_thread_stack = [];
    my $read_range        = $iApp->{__range_read};
    my $raw               = $iApp->{_param_raw};
    my $count             = $iApp->{__thread_count};
    my $loop              = int( $count / 100 ) + 1;
    my $omit              = $option->{noname};
    my $noname            = $iSetting->get('BBS_NONAME_NAME');

    my $st        = $iApp->{__read_from};
    my $to        = $iApp->{__read_to};
    my $parameter = {
        %$option,
        line    => $iApp->get_param_int('LINE'),
        nofirst => $iApp->{_param_nofirst},
        single  => $iApp->{_param_single},
        raw     => $raw,
    };

    my $max 
        = $iSetting->get_int('BBS_RES_MAX')
        || $iConfig->get_int('ResMAX')
        || 1000;

    if (    !$iApp->{_param_nofirst}
        and !$iApp->{_param_single}
        and $read_range->[0] == 1 )
    {
        my $first = $iThread->get(1);
        my $text  = $first->[4];
        $plugin->( $key, $count, 1, \$text, $parameter, $first );

        my $id   = $first->[3];
        my $name = $first->[0];
        my $date = $id ? ( join ' ', $first->[2], $id ) : $first->[2];
        my $textnt;
        if ($mobile) {
            ( $omit and $name eq $noname ) and $name = '';
            $textnt = $text;
            $textnt =~ s{<br\s*/?>}{\t}gxms;
            $textnt =~ s{<[^>]+>}{}gxms;
        }

        push @{$read_thread_stack},
            {
            index   => 1,
            resno   => 1,
            name    => $name,
            mail    => $first->[1],
            date    => $date,
            text    => $text,
            textNT  => $textnt,
            isFirst => 1,
            };
        shift @{$read_range};
    }

    for my $i ( @{$read_range} ) {
        my $res  = $iThread->get($i);
        my $text = $res->[4];
        $plugin->( $key, $count, $i, \$text, $parameter, $res );

        my $id   = $res->[3];
        my $name = $res->[0];
        my $date = $id ? ( join ' ', $res->[2], $id ) : $res->[2];
        my $textnt;
        if ($mobile) {
            ( $omit and $name eq $noname ) and $name = '';
            $textnt = $text;
            $textnt =~ s{<br\s*/?>}{\t}gxms;
            $textnt =~ s{<[^>]+>}{}gxms;
        }

        push @{$read_thread_stack},
            {
            index   => $i,
            resno   => $i,
            name    => $name,
            mail    => $res->[1],
            date    => $date,
            text    => $text,
            textNT  => $textnt,
            isFirst => 0,
            };
    }

    my $link = [];
    for ( my $i = 1; $i < $loop; $i++ ) {
        push @{$link},
            {
            start => ( $i * 100 + 1 ),
            end   => ( ( $i + 1 ) * 100 ),
            };
    }

    my ( $reader, $read_res, $read_res_prev ) = ( undef, 100, 99 );
    if ($mobile) {
        my $cnum 
            = $iApp->get_param_int('RES')
            || $iSetting->get_int('BBS_MB_CONTENTS_NUMBER')
            || 1;
        $read_res = $read_res_prev = $cnum;
    }
    my $server = $iRequest->get_app_uri();
    my $renderer = $iConfig->get('Renderer') || 'read.cgi';
    $reader = join( '/',
        $server, $renderer, $iBBS->get_name(), $iApp->{_param_key} );

    my $iMeta     = $iBBS->get_metadata_instance();
    my $iTemplate = $iBBS->get_template_instance(
        {   file    => $option->{template},
            request => $iRequest,
            setting => $iSetting,
        }
    );

    $mobile or $st--;
    my $r90c                = $max - int( $max / 10 );
    my $r95c                = $max - int( $max / 20 );
    my $r90                 = ( $count >= $r90c ? 1 : 0 );
    my $r95                 = ( $count >= $r95c ? 1 : 0 );
    my $r100                = ( $count > $max ? 1 : 0 );
    my $template_parameters = {
        ( map { %{$_} } @{ $iApp->{__template_tag_read} } ),
        '90'     => $r90,
        '95'     => $r95,
        '100'    => $r100,
        'R90'    => $r90,
        'R95'    => $r95,
        'R100'   => $r100,
        'R90C'   => $r90c,
        'R95C'   => $r95c,
        'Banner' => $mobile
        ? $iMeta->mobile_banner()
        : $iMeta->main_banner(),
        'BBS_RES_MAX'  => $max,
        'DatKByteSize' => int( $iThread->size() / 1024 ),
        'KEY'          => $key,
        'key'          => $key,
        'LinkStack'    => $link,

        #       'NameByCookie' => ( $iRequest->cookie('NAME') || '' ),
        'NameByCookie' => '',
        'NextEnd' => ( $to + $read_res > $count ? $count : $to + $read_res ),
        'NextStart' => $to + 1,

        #       'MailByCookie' => ( $iRequest->cookie('MAIL') || '' ),
        'MailByCookie' => '',
        'META'         => $iMeta->meta(),
        'PrevEnd'      => ( $st || 1 ),
        'PrevStart' =>
            ( $st - $read_res_prev < 1 ? 1 : $st - $read_res_prev ),
        'ReadCGIURI' => $reader,
        'ResCount'   => $count,
        'Subject'    => $iThread->get_subject(),
        'Thread'     => $read_thread_stack,
        'ZThread'    => $read_thread_stack,
        %{$template_vars},
    };

    if ($no_disk) {
        return $template_parameters;
    }
    else {
        my $cs = $iBBS->get_encoding();
        $iTemplate->param($template_parameters);
        $iRequest->send_http_header( 'text/html', $cs );
        $iTemplate->flush( $option->{filter} );
        return 1;
    }
}

sub redirect_error {
    my ( $iApp, $no_disk ) = @_;
    my $iBBS     = $iApp->{_bbs};
    my $iConfig  = $iApp->{_config};
    my $iKernel  = $iApp->{_kernel};
    my $iRequest = $iApp->{_request};
    my $key      = $iApp->{_param_key} || $iRequest->key;
    my $bbs      = $iBBS ? $iBBS->get_name() : '';
    my $server   = $iRequest->get_app_uri();
    my $renderer = $iConfig->get('Renderer') || 'read.cgi';
    my $reader   = join( '/', $server, $renderer, $bbs, $iApp->{_param_key} );
    my $archive_path  = '';
    my $archive_found = 0;
    my $banner        = '';
    my $dat_bytes     = 0;
    my $is_error      = 0;
    my $long          = '';
    my $meta          = '';
    my $pool_found    = 0;
    my $res_count     = 0;
    my $res_stack     = [];
    my $subject       = '';

    if ($iBBS) {
        $archive_path = $iBBS->url('kako');
        my $iArchive = $iBBS->get_archive_instance();
        ( $subject, $res_count ) = @{ $iArchive->get($key) };
        $res_count and $archive_found = 1;

        defined &{'Img0ch::Pool::Subject::new'}
            or require Img0ch::Pool::Subject;
        my $iPSubject = Img0ch::Pool::Subject->new($iBBS);
        ( $subject, $res_count ) = @{ $iPSubject->get($key) };
        if ($res_count) {
            defined &{'Img0ch::Pool::Thread::new'}
                or require Img0ch::Pool::Thread;
            my $iPThread = Img0ch::Pool::Thread->new( $iBBS, $key );
            $iPThread->load();
            my @load_reses = (1);
            $res_count != 1 and push @load_reses, $res_count;
            for my $i (@load_reses) {
                my $res  = $iPThread->get($i);
                my $id   = $res->[3];
                my $date = $id ? ( join ' ', $res->[2], $id ) : $res->[2];
                my $text = $res->[4];
                push @{$res_stack},
                    {
                    num     => $i,
                    resno   => $i,
                    name    => $res->[0],
                    mail    => $res->[1],
                    date    => $date,
                    text    => $text,
                    isFirst => ( $i == 1 ? 1 : 0 ),
                    };
            }
            $dat_bytes  = $iPThread->size();
            $pool_found = 1;
        }

        my $iMeta = $iBBS->get_metadata_instance();
        $banner = $iMeta->main_banner();
        $meta   = $iMeta->meta();
    }
    else {
        defined &{'Img0ch::Error::new'} or require Img0ch::Error;
        my $ip = $iRequest->ip_int();
        my $iError = Img0ch::Error->new( $iKernel, $key, $ip );
        $is_error = 1;
        $long = $iError->get( $iApp->{__error} || 'INVALID_POST' );
    }

    my $template_parameters = {
        ( map { %{$_} } @{ $iApp->{__template_tag_error} } ),
        Archive       => $archive_path,
        Banner        => $banner,
        DatByteSize   => $dat_bytes,
        DatKByteSize  => int( $dat_bytes / 1024 ),
        IsArchived    => $archive_found,
        IsError       => $is_error,
        IsMobile      => $iRequest->is_mobile_device(),
        IsPooled      => $pool_found,
        KEY           => $key,
        Token1        => substr( $key, 0, 4 ),
        Token2        => substr( $key, 0, 5 ),
        LongErrorStr  => $long,
        META          => $meta,
        ReadCGIURI    => $reader,
        ResCount      => $res_count,
        ShortErrorStr => $long,
        Subject       => $subject,
        Thread        => $res_stack,
        ZThread       => $res_stack,
    };

    if ($no_disk) {
        return $template_parameters;
    }
    else {
        my $iTemplate = Img0ch::Template->new(
            ( $iBBS || $iKernel ),
            {   file    => 'not_found',
                request => $iRequest,
                setting => $iApp->{_setting},
            }
        );
        $iTemplate->param($template_parameters);
        $iRequest->send_http_header('text/html');
        $iTemplate->flush();
        return 0;
    }
}

sub init {
    my ( $iApp, $option ) = @_;
    my $iConfig  = $iApp->{_config};
    my $iRequest = $iApp->{_request};
    my $mobile   = $option->{mobile};
    my ( $iBBS, $bbs, $key, $count, $last, $nofirst );

    $iConfig->get('Maintenance')
        and die 'img0ch MAINTENANCE: Please try again later.', "\n";
    $iRequest->init($iConfig);
    my $temp_range = [];

    if ( my $path_info = $iRequest->path_info() ) {
        my @path = split '/', $path_info;
        my $raw;

        shift @path;
        'test'   =~ /\A(\w+)\z/xms;
        $path[0] =~ /\A([\w\-]+)\z/xms;
        $bbs = $iApp->{_param_bbs} = $1;
        '0000000000' =~ /\A(\d+)\z/xms;
        $path[1] =~ /\A(\d{9,10})\z/xms;
        $key = $iApp->{_param_key} = $1;

        $iBBS = Img0ch::BBS->new( $iApp->{_kernel}, { bbs => $bbs } );
        if ( !$iBBS->get_id() ) {
            $iApp->{__error} = 'INVALID_POST';
            return 0;
        }
        $iApp->{_bbs} = $iBBS;

        my $iThread = $iBBS->get_thread_instance(0);
        $iThread->set_key($key);
        $count = $iThread->count();
        ( !$count and !$mobile ) and return 0;
        $iApp->{_thread}        = $iThread;
        $iApp->{__thread_count} = $count;

        my $extent_param_parser = sub {
            my ( $iApp, $parameter ) = @_;
            my $extend_param = $parameter;
            $iApp->{_query_with_extend_params} = $extend_param;
            my $extend_params = {};
            my $found         = 0;
            for my $chunk ( split /[&;]/xms, $extend_param ) {
                my ( $name, $value ) = split '=', $chunk;
                $value ||= '';
                $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack( 'H2', $1 )/gxmse;
                $value =~ tr/+/ /;
                $extend_params->{ $name || '' }
                    = Img0ch::Kernel::escape_html_entities($value);
                $found++;
            }
            %{ $iApp->{_extend_params} } = %{$extend_params};
            return $found;
        };

        if ( $key ne '0000000000' and @path >= 3 ) {
            if ($mobile) {
                my $query = '';
                $raw = $path[3] || '';
                $raw ? ( $query = $path[2] ) : ( $raw = $path[2] || '' );
                $extent_param_parser->( $iApp, $query );
            }
            else {
                $raw = $path[2] || '';
            }
        }
        elsif ( $bbs ne 'test' and @path >= 2 ) {
            my $query = '';
            $raw = $path[2] || ( $mobile ? $path[1] : '' );
            $query = $path[1] || '';
            $extent_param_parser->( $iApp, $query );
        }
        else {
            $raw = $path[2] || ( $mobile ? $path[1] : '' );
        }
        $iApp->{_param_raw} = $raw;
        $raw =~ s/[acCi]\z//xms;

        my $main_parser = sub {
            my ($chunk) = @_;
            '00000' =~ m{\A(\d)(\d)(\d)(\d)(\d)\z}xms;
            $chunk  =~ m{\A(l)?(\d*)(\-)?(\d*)?(n)?\z}xms
                or return [ 1, -1, 0, 0 ];
            my $read_recent = $1 ? 1 : 0;
            my $read_from = $2 || 1;
            my $read_to      = $3 ? $4 : $2;
            my $read_nofirst = $5 ? 1  : 0;
            return [ $read_from, $read_to, $read_recent, $read_nofirst ];
        };
        my $swap = sub {
            my ( $read_from, $read_to ) = @_;
            if ( $$read_from > $$read_to ) {
                my $temp = $$read_from;
                $$read_from = $$read_to;
                $$read_to   = $temp;
            }
        };
        my $plus_separated_parser = sub {
            my ($chunk) = @_;
            my @ret = ();
            if ( index( $chunk, '+' ) >= 0 ) {
                @ret = map { Img0ch::Kernel::intval($_) }
                    ( split '\+', $chunk );
            }
            return \@ret;
        };

        my $temp_range_hash = {};
        if ( index( $raw, ',' ) >= 0 ) {
            my ( $param_recent, $param_nofirst );
            for my $param ( split ',', $raw ) {
                $param or next;
                my ( $read_from, $read_to, $read_recent, $read_nofirst )
                    = @{ $main_parser->($param) };
                $read_to ||= $count;
                $swap->( \$read_from, \$read_to );
                map { $temp_range_hash->{$_} = 1 } (
                    ( $read_from .. $read_to ),
                    @{ $plus_separated_parser->($param) }
                );
                $param_recent  ||= $read_recent;
                $param_nofirst ||= $read_nofirst;
            }

            #warn scalar keys %$temp_range_hash, "\n";
            $iApp->{_param_ls}      = $last    = $param_recent;
            $iApp->{_param_nofirst} = $nofirst = $param_nofirst;
        }
        else {
            my @range = @{ $plus_separated_parser->($raw) };
            if ( !@range ) {
                my ( $read_from, $read_to, $read_recent, $read_nofirst )
                    = @{ $main_parser->($raw) };
                $iApp->{_param_ls} = $last = $read_recent;
                $read_to ||= $count;
                my $single = $iApp->{_param_single}
                    = ( $read_from == $read_to and !$read_recent ) ? 1 : 0;
                $iApp->{_param_nofirst} = $nofirst
                    = ( $single || $read_nofirst );
                $read_from > $count and $read_from = $count;
                $read_to > $count   and $read_to   = $count;
                $swap->( \$read_from, \$read_to );
                @range = ( $read_from .. $read_to );
            }
            map { $temp_range_hash->{$_} = 1 } @range;
        }
        delete $temp_range_hash->{'0'};
        @{$temp_range} = sort { $a <=> $b } keys %{$temp_range_hash};
    }
    else {
        $iApp->get_param('bbs') =~ /([\w\-]+)/xms;
        $bbs = $iApp->{_param_bbs} = $1;
        '0000000000' =~ /\A(\d+)\z/xms;
        $iApp->get_param('key') =~ /(\d{9,10})/;
        $key = $iApp->{_param_key} = $1;
        $iApp->{_param_raw} = '';

        $iBBS = Img0ch::BBS->new( $iApp->{_kernel}, { bbs => $bbs } );
        if ( !$iBBS->get_id() ) {
            $iApp->{__error} = 'INVALID_POST';
            return 0;
        }
        $iApp->{_bbs} = $iBBS;

        my $iThread = $iBBS->get_thread_instance(0);
        $iThread->set_key($key);
        $count = $iThread->count();
        ( !$count and !$mobile ) and return 0;
        $iApp->{_thread}        = $iThread;
        $iApp->{__thread_count} = $count;

        my $read_from = Img0ch::Kernel::intval( $iApp->get_param('st') ) || 1;
        my $read_to = Img0ch::Kernel::intval( $iApp->get_param('to') )
            || $count;
        $iApp->{_param_ls} 
            = $last
            || $iApp->get_param('last')
            || $iApp->get_param('ls');
        my $single = $iApp->{_param_single}
            = ( $read_from == $read_to and !$last ) ? 1 : 0;
        $iApp->{_param_nofirst} = $nofirst = ( $single
                || ( $iApp->get_param('nofirst') eq 'true' ? 1 : 0 ) );
        $read_from > $count and $read_from = $count;
        $read_to > $count   and $read_to   = $count;
        push @{$temp_range}, sort { $a <=> $b } ( $read_from .. $read_to );
    }

    $iApp->{_setting} = $iBBS->get_setting_instance();
    $iApp->{__now}    = $iRequest->now();
    $iApp->{__agent}  = $iRequest->agent();
    my $raw            = $iApp->{_param_raw};
    my $temp_read_from = $temp_range->[0] || 1;
    my $temp_read_to   = $temp_range->[-1] || $temp_read_from;
    my ( $real_read_from, $real_read_to, $read_by_const ) = (
        0, 0,
        (          $iApp->get_param_int('RES')
                || $iApp->{_setting}->get_int('BBS_MB_CONTENTS_NUMBER')
                || 1
        )
    );

    if ($mobile) {
        $read_by_const--;
        if ( $raw =~ /\A(n)?[acCi]*\z/xms ) {
            $last = 1;

            # $temp_read_to = 1001 ( ResMAX )
            #     =>
            # $temp_read_to = 10 ( $read_by_const )
            $nofirst = $iApp->{_param_nofirst} = $1 ? 1 : 0;
            $temp_read_to = $read_by_const;
        }
        elsif ( $raw =~ /l(\d+)(n)?/xms ) {
            my $read_range = $1;
            $last          = 1;
            $nofirst       = $iApp->{_param_nofirst} = $2 ? 1 : 0;
            $read_by_const = $read_range;
            $temp_read_to  = $read_range - 1;
        }
        else {
            $nofirst = $iApp->{_param_nofirst} = 1;
        }
    }

    if ($last) {
        my $first = $count - $temp_read_to;
        $real_read_from = $first > 0 ? $first : 1;
        ( !$mobile ) and $read_by_const = $temp_read_to;
        my $read_to = $real_read_from + $read_by_const;
        $real_read_to = $count > $read_to ? $read_to : $count;
        @{$temp_range} = $real_read_from .. $real_read_to;
        $iApp->{__read_from} = $real_read_from;
        $iApp->{__read_to}   = $real_read_to;
        if ( !$nofirst ) {
            $real_read_from = 1;
            $temp_range->[0] != 1 and unshift @{$temp_range}, 1;
        }
    }
    else {
        if ($nofirst) {
            $real_read_from = $temp_read_from > 0 ? $temp_read_from : 1;
        }
        else {
            $real_read_from = 1;
            $temp_range->[0] != 1 and unshift @{$temp_range}, 1;
        }
        $real_read_to
            = ( $temp_read_to < $count ) ? $temp_read_to
            : (
            ( $mobile and $temp_read_from != $temp_read_to )
            ? $real_read_from + $read_by_const
            : $count
            );
        $iApp->{__read_from} = $temp_read_from;
        $iApp->{__read_to}   = $real_read_to;
    }

    #    ( $is_mobile and $real_read_to - $real_read_from > $read_by_const )
    #        and $real_read_from = $real_read_to - $read_by_const;

    my $real_range = [];
    foreach my $i ( @{$temp_range} ) {
        ( $i < $real_read_from ) and next;
        ( $i > $real_read_to )   and next;
        ( $i > $count )          and next;
        push @{$real_range}, $i;
    }
    @{ $iApp->{__range_read} } = @{$real_range};

    if ( !$iConfig->get( $option->{disable} ) ) {
        my $iPlugin = $iBBS->get_plugin_instance($key);
        $iPlugin->do( $option->{type}, $iApp ) or return 0;
        $iApp->{_plugin} = $iPlugin;
    }

    return 1;
}

1;
__END__
