#/*
# *  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: User.pm 1165 2007-09-22 00:49:25Z hikarin $
#

package Zeromin::User;

use strict;
use Digest::MD5 qw();

BEGIN {
    my $pkg        = __PACKAGE__;
    my @privileges = qw(
        can_login
        can_enter_user_section
        can_create_user
        can_edit_user
        can_remove_user
        can_enter_ugroup_section
        can_create_ugroup
        can_edit_ugroup
        can_remove_ugroup
        can_enter_cap_section
        can_create_cap
        can_edit_cap
        can_remove_cap
        can_enter_cgroup_section
        can_create_cgroup
        can_edit_cgroup
        can_remove_cgroup
        can_ix_cgroup
        can_update_bbslist
        can_create_bbs
        can_update_bbs
        can_remove_bbs
        can_enter_setting_section
        can_change_main_setting
        can_change_color_setting
        can_change_restriction_setting
        can_change_other_setting
        can_change_zeroch_setting
        can_change_upload_setting
        can_change_import_setting
        can_stop_thread
        can_restart_thread
        can_autopool_thread
        can_move_thread
        can_remove_thread
        can_enter_pool_section
        can_create_pool
        can_recover_pool
        can_remove_pool
        can_enter_res_section
        can_edit_res
        can_abone_res
        can_remove_res
        can_freeze_file
        can_release_file
        can_remove_poll
        can_enter_archive_section
        can_create_archive
        can_remove_archive
        can_update_archive
        can_update_subject
        can_recreate_subject
        can_enter_meta_section
        can_edit_head
        can_edit_meta
        can_edit_foot
        can_edit_banner
        can_enter_control_section
        can_add_access
        can_remove_access
        can_add_ngword
        can_edit_ngword
        can_remove_ngword
        can_add_category
        can_edit_category
        can_remove_category
        can_enter_log_section
        can_view_error_log
        can_view_thread_log
        can_view_create_log
        can_view_zeromin_log
        can_enter_plugin_section
        can_regist_plugin
        can_config_plugin
    );

    for my $privilege (@privileges) {
        my $attr = '__' . $privilege;
        no strict 'refs';
        *{"${pkg}::${privilege}"} = sub { $_[0]->{$attr} || 0 }
    }
    no strict 'refs';
    *{"${pkg}::privilege_methods"} = sub { return \@privileges }
}

sub new {
    my ( $zClass, $iObject, $name ) = @_;
    my $class = ref $iObject || '';
    my $zUser = bless {}, $zClass;
    my $iKernel;

    if ( $class eq 'Img0ch::BBS' ) {
        $iKernel = $iObject->get_kernel();
        $zUser->{__bbs} = $iObject->get_id();
    }
    elsif ( $class eq 'Img0ch::Kernel' or $class eq 'Img0ch::Maple' ) {
        $iKernel = $iObject;
        $zUser->{__bbs} = 0;
    }
    else {
        Img0ch::Kernel->throw_exception(
            'Img0ch::BBS or Img0ch::Maple(Img0ch::Kernel) not given');
    }

    my $repos_path = $iKernel->get_repos_path('user');
    my $iRepos     = $iKernel->get_repos($repos_path);
    $zUser->{__repos}      = $iRepos;
    $zUser->{__group_pool} = {};
    $zUser->{__user_pool}  = {};
    $zUser->{__id}         = 0;
    $zUser->{__name}       = $name;
    $zUser->{__kernel}     = $iKernel;
    $zUser->{__group_id}   = $iRepos->get_int('I:U:G._');

    if ( $iKernel->get_config()->get('compatible.user') ) {
        require Zeromin::Compat::User;
        Zeromin::Compat::User::update( $zUser, $iKernel );
    }
    return $zUser;
}

sub load { $_[0]->{__repos}->load(); return 1 }

sub save {
    my ($zUser) = @_;
    my $iRepos = $zUser->{__repos};

    my $ucount = $zUser->get_user_count();
    my $up     = $zUser->{__user_pool};
    for my $id ( keys %{$up} ) {
        my $value = $up->{$id};
        my $name = $value->{name} || '';
        $name =~ tr/\r\n//d;
        $iRepos->set( "I:U:I.${id}.name", $name );
        $iRepos->set( "I:U:I.${id}.mail", $value->{mail} || '' );
        $iRepos->set( "I:U:I.${id}.pass", $value->{pass} || '' );
        $iRepos->set( "I:U:I.${id}.gid",  $value->{gid} || 0 );
        $iRepos->set( "I:U:I.${name}.id", $id );
        $ucount++;
    }

    my $gp = $zUser->{__group_pool};
    for my $id ( keys %{$gp} ) {
        my $value     = $gp->{$id};
        my $name      = $value->{name} || '';
        my $privilege = $value->{privilege};
        $name =~ tr/\r\n//d;
        $iRepos->set( "I:U:G.${id}.name",        $name );
        $iRepos->set( "I:U:G.${id}.role",        $value->{role} || 1 );
        $iRepos->set( "I:U:G.${id}.role.user",   $privilege->{user} || 0 );
        $iRepos->set( "I:U:G.${id}.role.cap",    $privilege->{cap} || 0 );
        $iRepos->set( "I:U:G.${id}.role.bbs",    $privilege->{bbs} || 0 );
        $iRepos->set( "I:U:G.${id}.role.thread", $privilege->{thread} || 0 );
        $iRepos->set( "I:U:G.${id}.role.res",    $privilege->{res} || 0 );
        $iRepos->set( "I:U:G.${id}.role.pool",   $privilege->{pool} || 0 );
        $iRepos->set( "I:U:G.${id}.role.archive",
            $privilege->{archive} || 0 );
        $iRepos->set( "I:U:G.${id}.role.subject",
            $privilege->{subject} || 0 );
        $iRepos->set( "I:U:G.${id}.role.setting",
            $privilege->{setting} || 0 );
        $iRepos->set( "I:U:G.${id}.role.meta", $privilege->{meta} || 0 );
        $iRepos->set( "I:U:G.${id}.role.control",
            $privilege->{control} || 0 );
        $iRepos->set( "I:U:G.${id}.role.category",
            $privilege->{category} || 0 );
        $iRepos->set( "I:U:G.${id}.role.view",   $privilege->{view}   || 0 );
        $iRepos->set( "I:U:G.${id}.role.plugin", $privilege->{plugin} || 0 );
        $iRepos->set( "I:U:G.${name}.id",        $id );

        for my $bbs ( @{ $value->{bbs} } ) {
            $iRepos->set( "I:U:G:S.${id}.${bbs}", 1 );
        }
    }

    $iRepos->set( 'I:U:I._', $ucount );
    $iRepos->set( 'I:U:G._', $zUser->{__group_id} );
    $iRepos->save();
    1;
}

sub is_initialized { $_[0]->{__group_id} != 0 }

sub is_valid {
    my ( $zUser, $pass, $name ) = @_;

    $pass or return 0;
    $name ||= $zUser->{__name};
    my $iRepos = $zUser->{__repos};
    my $uid    = Digest::MD5::md5_base64($name);
    $iRepos->get("I:U:I.${uid}.pass") eq $pass or return 0;

    my $bbs    = $zUser->{__bbs};
    my $gid    = $iRepos->get_int("I:U:I.${uid}.gid") or return 0;
    my $global = $iRepos->get("I:U:G:S.${gid}.0");
    my $scope  = $iRepos->get("I:U:G:S.${gid}.${bbs}") || $global;
    $global and $zUser->{__bbs} = 0;

    if ( !$scope ) {
        $zUser->{__id}        = $uid;
        $zUser->{__gid}       = $gid;
        $zUser->{__name}      = $name;
        $zUser->{__mail}      = $iRepos->get("I:U:I.${uid}.mail");
        $zUser->{__pass}      = $pass;
        $zUser->{__can_login} = $iRepos->get_int("I:U:G.${gid}.role");
        return 1;
    }
    elsif ( my $role = $iRepos->get_int("I:U:G.${gid}.role") ) {
        $zUser->{__can_login} = $role;
        my $r_user = $iRepos->get_int("I:U:G.${gid}.role.user");
        $zUser->{__can_enter_user_section}   = $r_user      ? 1 : 0;
        $zUser->{__can_enter_ugroup_section} = $r_user > 7  ? 1 : 0;
        $zUser->{__can_create_user}          = $r_user & 1  ? 1 : 0;
        $zUser->{__can_edit_user}            = $r_user & 2  ? 1 : 0;
        $zUser->{__can_remove_user}          = $r_user & 4  ? 1 : 0;
        $zUser->{__can_create_ugroup}        = $r_user & 8  ? 1 : 0;
        $zUser->{__can_edit_ugroup}          = $r_user & 16 ? 1 : 0;
        $zUser->{__can_remove_ugroup}        = $r_user & 32 ? 1 : 0;
        $zUser->{__can_ix_user}              = $r_user & 7  ? 1 : 0;
        $zUser->{__can_ix_ugroup}            = $r_user & 56 ? 1 : 0;
        my $r_cap = $iRepos->get_int("I:U:G.${gid}.role.cap");
        $zUser->{__can_enter_cap_section}    = $r_cap       ? 1 : 0;
        $zUser->{__can_enter_cgroup_section} = $r_cap > 7   ? 1 : 0;
        $zUser->{__can_create_cap}           = $r_cap & 1   ? 1 : 0;
        $zUser->{__can_edit_cap}             = $r_cap & 2   ? 1 : 0;
        $zUser->{__can_remove_cap}           = $r_cap & 4   ? 1 : 0;
        $zUser->{__can_create_cgroup}        = $r_cap & 8   ? 1 : 0;
        $zUser->{__can_edit_cgroup}          = $r_cap & 16  ? 1 : 0;
        $zUser->{__can_remove_cgroup}        = $r_cap & 32  ? 1 : 0;
        $zUser->{__can_ix_cap}               = $r_user & 7  ? 1 : 0;
        $zUser->{__can_ix_cgroup}            = $r_user & 56 ? 1 : 0;
        my $r_bbs = $iRepos->get_int("I:U:G.${gid}.role.bbs");
        $zUser->{__can_create_bbs}     = $r_bbs & 1 ? 1 : 0;
        $zUser->{__can_update_bbs}     = $r_bbs & 2 ? 1 : 0;
        $zUser->{__can_remove_bbs}     = $r_bbs & 4 ? 1 : 0;
        $zUser->{__can_update_bbslist} = $r_bbs & 8 ? 1 : 0;
        my $r_pref = $iRepos->get_int("I:U:G.${gid}.role.setting");
        $zUser->{__can_enter_setting_section}      = $r_pref      ? 1 : 0;
        $zUser->{__can_change_main_setting}        = $r_pref & 1  ? 1 : 0;
        $zUser->{__can_change_color_setting}       = $r_pref & 2  ? 1 : 0;
        $zUser->{__can_change_restriction_setting} = $r_pref & 4  ? 1 : 0;
        $zUser->{__can_change_other_setting}       = $r_pref & 8  ? 1 : 0;
        $zUser->{__can_change_zeroch_setting}      = $r_pref & 16 ? 1 : 0;
        $zUser->{__can_change_upload_setting}      = $r_pref & 32 ? 1 : 0;
        $zUser->{__can_change_import_setting}      = $r_pref & 64 ? 1 : 0;
        my $r_thread = $iRepos->get_int("I:U:G.${gid}.role.thread");
        $zUser->{__can_stop_thread}     = $r_thread & 1  ? 1 : 0;
        $zUser->{__can_restart_thread}  = $r_thread & 2  ? 1 : 0;
        $zUser->{__can_autopool_thread} = $r_thread & 4  ? 1 : 0;
        $zUser->{__can_move_thread}     = $r_thread & 8  ? 1 : 0;
        $zUser->{__can_remove_thread}   = $r_thread & 16 ? 1 : 0;
        my $r_pool = $iRepos->get_int("I:U:G.${gid}.role.pool");
        $zUser->{__can_enter_pool_section} = $r_pool     ? 1 : 0;
        $zUser->{__can_create_pool}        = $r_pool & 1 ? 1 : 0;
        $zUser->{__can_recover_pool}       = $r_pool & 2 ? 1 : 0;
        $zUser->{__can_remove_pool}        = $r_pool & 4 ? 1 : 0;
        my $r_res = $iRepos->get_int("I:U:G.${gid}.role.res");
        $zUser->{__can_enter_res_section} = $r_res      ? 1 : 0;
        $zUser->{__can_abone_res}         = $r_res & 1  ? 1 : 0;
        $zUser->{__can_remove_res}        = $r_res & 2  ? 1 : 0;
        $zUser->{__can_edit_res}          = $r_res & 4  ? 1 : 0;
        $zUser->{__can_freeze_file}       = $r_res & 8  ? 1 : 0;
        $zUser->{__can_release_file}      = $r_res & 16 ? 1 : 0;
        $zUser->{__can_remove_poll}       = $r_res & 32 ? 1 : 0;
        my $r_archive = $iRepos->get_int("I:U:G.${gid}.role.archive");
        $zUser->{__can_enter_archive_section} = $r_archive     ? 1 : 0;
        $zUser->{__can_create_archive}        = $r_archive & 1 ? 1 : 0;
        $zUser->{__can_remove_archive}        = $r_archive & 2 ? 1 : 0;
        $zUser->{__can_update_archive}        = $r_archive & 4 ? 1 : 0;
        my $r_subject = $iRepos->get_int("I:U:G.${gid}.role.subject");
        $zUser->{__can_update_subject}   = $r_subject & 1 ? 1 : 0;
        $zUser->{__can_recreate_subject} = $r_subject & 2 ? 1 : 0;
        my $r_meta = $iRepos->get_int("I:U:G.${gid}.role.meta");
        $zUser->{__can_enter_meta_section} = $r_meta     ? 1 : 0;
        $zUser->{__can_edit_head}          = $r_meta & 1 ? 1 : 0;
        $zUser->{__can_edit_meta}          = $r_meta & 2 ? 1 : 0;
        $zUser->{__can_edit_foot}          = $r_meta & 4 ? 1 : 0;
        $zUser->{__can_edit_banner}        = $r_meta & 8 ? 1 : 0;
        my $r_cntl = $iRepos->get_int("I:U:G.${gid}.role.control");
        $zUser->{__can_enter_control_section} = $r_cntl      ? 1 : 0;
        $zUser->{__can_add_access}            = $r_cntl & 1  ? 1 : 0;
        $zUser->{__can_remove_access}         = $r_cntl & 2  ? 1 : 0;
        $zUser->{__can_add_ngword}            = $r_cntl & 4  ? 1 : 0;
        $zUser->{__can_edit_ngword}           = $r_cntl & 8  ? 1 : 0;
        $zUser->{__can_remove_ngword}         = $r_cntl & 16 ? 1 : 0;
        my $r_cat = $iRepos->get_int("I:U:G.${gid}.role.category");
        $zUser->{__can_add_category}    = $r_cat & 1 ? 1 : 0;
        $zUser->{__can_edit_category}   = $r_cat & 2 ? 1 : 0;
        $zUser->{__can_remove_category} = $r_cat & 4 ? 1 : 0;
        my $r_log = $iRepos->get_int("I:U:G.${gid}.role.view");
        $zUser->{__can_enter_log_section} = $r_log     ? 1 : 0;
        $zUser->{__can_view_error_log}    = $r_log & 1 ? 1 : 0;
        $zUser->{__can_view_create_log}   = $r_log & 2 ? 1 : 0;
        $zUser->{__can_view_thread_log}   = $r_log & 4 ? 1 : 0;
        $zUser->{__can_view_zeromin_log}  = $r_log & 8 ? 1 : 0;
        my $r_plugin = $iRepos->get_int("I:U:G.${gid}.role.plugin");
        $zUser->{__can_enter_plugin_section} = $r_plugin     ? 1 : 0;
        $zUser->{__can_regist_plugin}        = $r_plugin & 1 ? 1 : 0;
        $zUser->{__can_config_plugin}        = $r_plugin & 2 ? 1 : 0;
        $zUser->{__id}                       = $uid;
        $zUser->{__gid}                      = $gid;
        $zUser->{__name}                     = $name;
        $zUser->{__mail} = $iRepos->get("I:U:G.${uid}.mail");
        $zUser->{__pass} = $pass;
        return 1;
    }
    0;
}

sub get_bbs_range {
    my ( $zUser, $user_id ) = @_;
    $user_id ||= $zUser->{__id};

    my $iRepos = $zUser->{__repos};
    my $gid    = $zUser->get_user_gid($user_id);
    $iRepos->get("I:U:G:S.${gid}.0") and return [];

    my $result = [];
    $iRepos->iterate(
        sub {
            my ( $key, $value, $gid, $result ) = @_;
            if ( $key =~ /\AI:U:G:S.$gid.(\d+)\z/xms ) {
                push @$result, $1;
            }
            return 0;
        },
        $gid,
        $result,
    );
    return $result;
}

sub get_current_user {
    {   id   => $_[0]->{__id},
        name => $_[0]->{__name},
        mail => $_[0]->{__mail},
        bbs  => $_[0]->{__bbs},
        gid  => $_[0]->{__gid},
    };
}

sub get_user_count {
    my ($zUser) = @_;
    my $iRepos  = $zUser->{__repos};
    my $count   = $iRepos->get_int('I:U:I._');

    if ( !$count ) {
        my $result = {};
        $iRepos->iterate(
            sub {
                my ( $key, $value, $result ) = @_;
                if ( $key =~ /\AI:U:I.([^\.]+)\.\w+\z/xms ) {
                    $result->{$1} = 1;
                }
                return 0;
            },
            $result,
        );
        $count = scalar keys %$result;
    }
    return $count;
}

sub have_privilege {
    my ( $zUser, $bbs_id ) = @_;
    my $iRepos = $zUser->{__repos};
    my $gid    = $zUser->{__gid};
    $bbs_id = Img0ch::Kernel::intval($bbs_id);
    $iRepos->get("I:U:G:S.${gid}.${bbs_id}")
        || $iRepos->get("I:U:G:S.${gid}.0");
}

sub add {
    my ( $zUser, $user ) = @_;
    $zUser->get_group_name( $user->{gid} ) or return 0;
    my $name = $user->{name};
    $name =~ tr/\r\n//d;
    $user->{name} = $name;
    my $uid = Digest::MD5::md5_base64($name);
    $zUser->get_user_name($uid) eq $name and return 0;
    $zUser->_plugin( 'zeromin.create.user', [ $uid, $user ] );
    $zUser->{__user_pool}->{$uid} = $user;
    return 1;
}

sub edit {
    my ( $zUser, $id, $user ) = @_;
    $zUser->get_user_gid($id) || return 0;
    my $up = $zUser->{__user_pool};
    delete $user->{name};

    if ( !exists $up->{$id} ) {
        my $iRepos = $zUser->{__repos};
        my $gid    = $iRepos->get("I:U:I.${id}.gid");
        if ($gid) {
            $up->{$id} = {
                name => $iRepos->get("I:U:I.${id}.name"),
                mail => $iRepos->get("I:U:I.${id}.mail"),
                pass => $iRepos->get("I:U:I.${id}.pass"),
                gid  => $gid,
            };
        }
        else {
            return 0;
        }
    }
    my $one = $up->{$id};
    while ( my ( $key, $value ) = each %$user ) {
        $one->{$key} = $value;
    }
    $zUser->_plugin( 'zeromin.update.user', [ $id, $one ] );
    $zUser->{__user_pool}->{$id} = $one;
    1;
}

sub remove {
    my ( $zUser, $uid ) = @_;
    my $up = $zUser->{__user_pool};
    exists $up->{$uid} and delete $up->{$uid};

    my $iRepos = $zUser->{__repos};
    my $name   = $iRepos->remove("I:U:I.${uid}.name");
    $iRepos->remove("I:U:I.${uid}.mail");
    $iRepos->remove("I:U:I.${uid}.pass");
    $iRepos->remove("I:U:I.${uid}.gid");
    $iRepos->remove("I:U:I.${name}.id");
    $iRepos->set( 'I:U:I._', $zUser->get_user_count() - 1 );
    $zUser->_plugin( 'zeromin.remove.user', [$uid] );
    1;
}

sub get_user {
    my ( $zUser, $id ) = @_;
    my $iRepos = $zUser->{__repos};
    my $gid    = $iRepos->get("I:U:I.${id}.gid");

    if ($gid) {
        {   name  => $iRepos->get("I:U:I.${id}.name"),
            mail  => $iRepos->get("I:U:I.${id}.mail"),
            id    => $id,
            gid   => $gid,
            gname => $zUser->get_group_name($gid),
        };
    }
    else {
        {   name  => '',
            mail  => '',
            id    => undef,
            gid   => 0,
            gname => '',
        };
    }
}

sub get_users {
    my ($zUser) = @_;
    my $iRepos  = $zUser->{__repos};
    my $cache   = {};
    my $ret     = [];

    $iRepos->iterate(
        sub {
            my ( $key, $value, $cache ) = @_;
            if ( $key =~ /\AI:U:I\.(.+?)\.(\w+)\z/xms ) {
                $2 eq 'id' and return 0;
                $cache->{$1} ||= {};
                $cache->{$1}->{$2} = $$value;
            }
            return 0;
        },
        $cache
    );

    while ( my ( $id, $info ) = each %{$cache} ) {
        my $gid = $info->{gid};
        push @$ret,
            {
            name  => $info->{name},
            mail  => $info->{mail},
            id    => $id,
            gid   => $gid,
            gname => $zUser->get_group_name($gid)
            };
    }
    $ret;
}

sub get_users_with_page {
    my ( $zUser, $item_per_page, $offset ) = @_;

    require Data::Page;
    my $entries = $zUser->get_users();
    my $page = Data::Page->new( scalar @$entries, $item_per_page, $offset );
    return ( [ $page->splice($entries) ], $page );
}

sub get_user_id {
    my ( $zUser, $name ) = @_;
    while ( my ( $uid, $value ) = each %{ $zUser->{__user_pool} } ) {
        ( $value->{name} || '' ) eq $name and return $uid;
    }

    my $iRepos = $zUser->{__repos};
    my $id     = $iRepos->get("I:U:I.${name}.id");
    $id and return $id;

    $iRepos->iterate(
        sub {
            my ( $key, $value, $name, $id ) = @_;
            if ( $key =~ /\AI:U:I\.(.+?)\.name\z/xms and $$value eq $name ) {
                $$id = $1;
            }
            return 0;
        },
        $name,
        \$id
    );
    return $id || 0;
}

sub get_user_name { shift->_get( 'user', 'name', @_ ) }

sub get_user_mail { shift->_get( 'user', 'mail', @_ ) }

sub get_user_pass { shift->_get( 'user', 'pass', @_ ) }

sub get_user_gid {
    Img0ch::Kernel::intval( shift->_get( 'user', 'gid', @_ ) );
}

sub get_user_belongs { shift->_get( 'user', 'bbs', @_ ) }

sub add_group {
    my ( $zUser, $group ) = @_;
    my $iKernel = $zUser->{__kernel};
    my $bbs     = $group->{bbs};
    my $seen    = {};

    defined $bbs or return 0;
    ( ref $bbs || '' ) eq 'ARRAY' or $bbs = [$bbs];
    for my $scope (@$bbs) {
        $scope = Img0ch::Kernel::intval($scope);
        if ($scope) {
            my $iBBS = Img0ch::BBS->new( $iKernel, { id => $scope } );
            my $id = $iBBS->get_id() || return 0;
            $seen->{$id} = 1;
        }
        else {
            $seen->{$scope} = 1;
        }
    }
    $group->{name} =~ tr/\r\n//d;
    $group->{bbs} = [ grep { $_ != 0 } keys %$seen ];
    @{ $group->{bbs} } or @{ $group->{bbs} } = (0);

    my $gid = ++$zUser->{__group_id};
    $zUser->_plugin( 'zeromin.create.ugroup', [ $gid, $group ] );
    $zUser->{__group_pool}->{$gid} = $group;
    return $gid;
}

sub edit_group {
    my ( $zUser, $gid, $group ) = @_;
    my $gp = $zUser->{__group_pool};

    if ( exists $group->{bbs} ) {
        my $iKernel = $zUser->{__kernel};
        my $bbs     = $group->{bbs};
        my $seen    = {};

        ( ref $bbs || '' ) eq 'ARRAY' or $bbs = [$bbs];
        for my $scope (@$bbs) {
            $scope = Img0ch::Kernel::intval($scope);
            if ($scope) {
                my $iBBS = Img0ch::BBS->new( $iKernel, { id => $scope } );
                my $id = $iBBS->get_id() || return 0;
                $seen->{$id} = 1;
            }
            else {
                $seen->{$scope} = 1;
            }
        }
        $group->{bbs} = [ grep { $_ != 0 } keys %$seen ];
        @{ $group->{bbs} } or @{ $group->{bbs} } = (0);

        $zUser->{__repos}->iterate(
            sub {
                my ( $key, $value, $gid ) = @_;
                $key =~ /\AI:U:G:S\.$gid\./xms and return -1;
                return 0;
            },
            $gid
        );
    }
    if ( !exists $gp->{$gid} ) {
        my $name = $zUser->{__repos}->get("I:U:G.${gid}.name");
        if ($name) {
            $gp->{$gid} = {
                bbs  => $group->{bbs},
                name => $name,
            };
        }
        else {
            return 0;
        }
    }

    my $one = $gp->{$gid};
    if ( exists $group->{privilege} ) {
        my $privilege = delete $group->{privilege};
        my $priv      = $one->{privilege};
        while ( my ( $key, $value ) = each %$privilege ) {
            $priv->{$key} = $value;
        }
        $one->{privilege} = $priv;
    }
    while ( my ( $key, $value ) = each %$group ) {
        $one->{$key} = $value;
    }
    $zUser->_plugin( 'zeromin.update.ugroup', [ $gid, $one ] );
    $zUser->{__group_pool}->{$gid} = $one;
    1;
}

sub remove_group {
    my ( $zUser, $gid ) = @_;
    my $iRepos = $zUser->{__repos};
    my $count  = 0;
    my $gp     = $zUser->{__group_pool};
    $iRepos->iterate(
        sub {
            my ( $key, $value, $gid, $count ) = @_;
            if ( $key =~ /\AI:U:I\.(?:.+)\.gid\z/ and $$value == $gid ) {
                $$count++;
            }
            return 0;
        },
        $gid,
        \$count
    );
    $count and return 0;

    exists $gp->{$gid} and delete $gp->{$gid};
    my $name = $iRepos->remove("I:U:G.${gid}.name");
    $iRepos->remove("I:U:G.${name}.id");
    $iRepos->iterate(
        sub {
            my ( $key, $value, $gid ) = @_;
            $key =~ /\AI:U:G\.$gid/xms   and return -1;
            $key =~ /\AI:U:G:S\.$gid/xms and return -1;
            return 0;
        },
        $gid
    );
    $zUser->_plugin( 'zeromin.remove.ugroup', [$gid] );
    return 1;
}

sub get_group {
    my ( $zUser, $gid ) = @_;
    my $iRepos = $zUser->{__repos};
    my $name   = $iRepos->get("I:U:G.${gid}.name");

    if ($name) {
        my ( $count, $scope_stack ) = ( 0, [] );
        $iRepos->iterate(
            sub {
                my ( $key, $value, $gid, $count ) = @_;
                if ( $key =~ /\AI:U:I\.(?:.+)\.gid\z/ and $$value == $gid ) {
                    $$count++;
                }
                return 0;
            },
            $gid,
            \$count
        );
        $iRepos->iterate(
            sub {
                my ( $key, $value, $gid, $scope_stack ) = @_;
                if ( $key =~ /\AI:U:G:S\.$gid\.(.+?)\z/xms ) {
                    push @$scope_stack, $1;
                }
                return 0;
            },
            $gid,
            $scope_stack
        );
        {   bbs       => $scope_stack,
            count     => $count,
            id        => $gid,
            name      => $name,
            privilege => {
                user     => $iRepos->get_int("I:U:G.${gid}.role.user"),
                cap      => $iRepos->get_int("I:U:G.${gid}.role.cap"),
                bbs      => $iRepos->get_int("I:U:G.${gid}.role.bbs"),
                thread   => $iRepos->get_int("I:U:G.${gid}.role.thread"),
                res      => $iRepos->get_int("I:U:G.${gid}.role.res"),
                pool     => $iRepos->get_int("I:U:G.${gid}.role.pool"),
                archive  => $iRepos->get_int("I:U:G.${gid}.role.archive"),
                setting  => $iRepos->get_int("I:U:G.${gid}.role.setting"),
                subject  => $iRepos->get_int("I:U:G.${gid}.role.subject"),
                meta     => $iRepos->get_int("I:U:G.${gid}.role.meta"),
                control  => $iRepos->get_int("I:U:G.${gid}.role.control"),
                category => $iRepos->get_int("I:U:G.${gid}.role.category"),
                view     => $iRepos->get_int("I:U:G.${gid}.role.view"),
                plugin   => $iRepos->get_int("I:U:G.${gid}.role.plugin"),
            },
        };
    }
    else {
        {   bbs       => [0],
            count     => 0,
            id        => 0,
            name      => '',
            privilege => {
                user     => 0,
                cap      => 0,
                bbs      => 0,
                thread   => 0,
                res      => 0,
                pool     => 0,
                archive  => 0,
                setting  => 0,
                subject  => 0,
                meta     => 0,
                control  => 0,
                category => 0,
                view     => 0,
                plugin   => 0,
            },
        };
    }
}

sub get_groups {
    my ($zUser)     = @_;
    my $iRepos      = $zUser->{__repos};
    my $group_cache = {};
    my $scope_cache = {};
    my $ret         = [];

    $iRepos->iterate(
        sub {
            my ( $key, $value, $group_cache, $scope_cache ) = @_;
            if ( $key =~ /\AI:U:I\.(?:.+)\.gid\z/ ) {
                $group_cache->{$$value} ||= {};
                $group_cache->{$$value}->{count}++;
            }
            elsif ( $key =~ /\AI:U:G\.(.+?)\.(.+?)\z/xms ) {
                my ( $gid, $key ) = ( $1, $2 );
                $key eq 'id' and return 0;
                $group_cache->{$gid} ||= {};
                $group_cache->{$gid}->{$key} = $$value;
            }
            elsif ( $key =~ /\AI:U:G:S\.(.+?)\.(.+)\z/xms ) {
                my ( $gid, $bbs ) = ( $1, $2 );
                $scope_cache->{$gid} ||= [];
                push @{ $scope_cache->{$gid} }, $bbs;
            }
            return 0;
        },
        $group_cache,
        $scope_cache
    );

    while ( my ( $id, $info ) = each %{$group_cache} ) {
        push @$ret,
            {
            bbs => ( $scope_cache->{$id} || [0] ),
            count     => Img0ch::Kernel::intval( $info->{count} ),
            id        => $id,
            name      => $info->{name},
            privilege => {
                user     => $info->{'role.user'},
                cap      => $info->{'role.cap'},
                bbs      => $info->{'role.bbs'},
                thread   => $info->{'role.thread'},
                res      => $info->{'role.res'},
                pool     => $info->{'role.pool'},
                archive  => $info->{'role.archive'},
                setting  => $info->{'role.setting'},
                subject  => $info->{'role.subject'},
                meta     => $info->{'role.meta'},
                control  => $info->{'role.control'},
                category => $info->{'role.category'},
                view     => $info->{'role.view'},
                plugin   => $info->{'role.plugin'},
            },
            };
    }
    $ret;
}

sub get_groups_with_page {
    my ( $zUser, $item_per_page, $offset ) = @_;

    require Data::Page;
    my $entries = $zUser->get_groups();
    my $page = Data::Page->new( scalar @$entries, $item_per_page, $offset );
    return ( [ $page->splice($entries) ], $page );
}

sub get_group_id {
    my ( $zUser, $name ) = @_;
    while ( my ( $gid, $value ) = each %{ $zUser->{__group_pool} } ) {
        ( $value->{name} || '' ) eq $name and return $gid;
    }

    my $iRepos = $zUser->{__repos};
    my $id     = $iRepos->get("I:U:G.${name}.id");
    $id and return $id;

    $iRepos->iterate(
        sub {
            my ( $key, $value, $name, $id ) = @_;
            if ( $key =~ /\AI:U:G\.(.+?)\.name\z/xms and $$value eq $name ) {
                $$id = $1;
            }
            return 0;
        },
        $name,
        \$id
    );
    return $id || 0;
}

sub get_group_name { shift->_get( 'group', 'name', @_ ) }

sub get_group_belongs {
    Img0ch::Kernel::intval( shift->_get( 'group', 'bbs', @_ ) );
}

sub remove_from_bbs {
    my ( $zUser, $bbs_id ) = @_;
    $bbs_id or return 0;

    my $iRepos = $zUser->{__repos};
    $iRepos->iterate(
        sub {
            my ( $key, $value, $bbs_id ) = @_;
            $key =~ /\AI:U:G:S\.[^\.]+\.$bbs_id\z/xms and return -1;
            return 0;
        },
        $bbs_id
    );
    $iRepos->save();
    return 1;
}

sub _get {
    my ( $zUser, $type, $accessor, $id ) = @_;
    my $p = $zUser->{"__${type}_pool"};
    $id ||= '';

    if ( $p->{$id} ) {
        $p->{$id}->{$accessor};
    }
    else {
        my $prefix = { 'user' => 'I:U:I', 'group' => 'I:U:G' }->{$type};
        my $iRepos = $zUser->{__repos};
        $iRepos->get("${prefix}.${id}.${accessor}");
    }
}

sub _plugin {
    my ( $zUser, $at, $argument ) = @_;
    my $iKernel = $zUser->{__kernel};
    require Zeromin::Plugin;
    my $zPlugin = Zeromin::Plugin->new($iKernel);
    $zPlugin->do( $at, $zUser, $argument );
    return;
}

1;
__END__
