package MogileFS::Class;
use strict;

my %singleton;     # dmid -> classid -> MogileFS::Class
my $last_load = 0;

# return MogileFS::Class object for a given fid id/obj
sub of_fid {
    my ($pkg, $fid) = @_;
    return undef unless $fid;
    # make $fid into a FID object:
    $fid = MogileFS::FID->new($fid) unless ref $fid;
    return undef unless $fid->exists;
    my $cl = $pkg->of_dmid_classid($fid->dmid, $fid->classid);
    return $cl if $cl;
    # return the default class for this file, not undef.  this should
    # always return a valid class for a valid FID.  files need to
    # always have a mindevcount (default of 2), repl policy, etc.
    return $pkg->of_dmid_classid($fid->dmid, 0);
}

# return MogileFS::Class, given a dmid and classid.  or returns the
# default class, if classid is bogus.
sub of_dmid_classid {
    my ($pkg, $dmid, $classid) = @_;
    return $singleton{$dmid}{$classid} if
         $singleton{$dmid} &&
         $singleton{$dmid}{$classid} &&
         $singleton{$dmid}{$classid}->{_loaded};
    $pkg->reload_classes;
    return $singleton{$dmid}{$classid} if
        $singleton{$dmid} &&
        $singleton{$dmid}{$classid};
    return undef;
}

# marks everything dirty, triggering a reload, but doesn't actually
# reload now.  will happen later, next time somebody loads something.
sub invalidate_cache {
    my $pkg = shift;
    $last_load = 0;
    $pkg->_foreach_singleton(sub {
        my $cl = shift;
        $cl->{_loaded} = 0;
    });
    if (my $worker = MogileFS::ProcManager->is_child) {
        $worker->invalidate_meta("class");
    }
}

sub check_cache {
    my $pkg = shift;
    my $now = time();
    return if $last_load > $now - 5;
    MogileFS::Class->reload_classes;
}

sub reload_classes {
    my $pkg = shift;
    my $now = time();

    # mark everything as invalid for now
    $pkg->_foreach_singleton(sub {
        my ($cl, $dmid, $clid) = @_;
        $cl->{_loaded} = 0;
    });

    foreach my $row (Mgd::get_store()->get_all_classes) {
        my $cl =
            ($singleton{$row->{dmid}}{$row->{classid}} ||=
             bless {
                 dmid        => $row->{dmid},
                 classid     => $row->{classid},
                 name        => $row->{classname},
                 mindevcount => $row->{mindevcount},
             }, $pkg);
        $cl->{_loaded} = 1;
    }

    # install the default classes (classid=0)
    my $default_min = MogileFS->config('default_mindevcount');
    foreach my $dom (MogileFS::Domain->domains) {
        my $dmid = $dom->id;
        my $cl =
            ($singleton{$dmid}{0} ||=
             bless {
                 dmid        => $dmid,
                 classid     => 0,
                 name        => "default",
                 mindevcount => $default_min,
             }, $pkg);
            $cl->{_loaded} = 1;
    }

    # delete any singletons that weren't just loaded
    $pkg->_foreach_singleton(sub {
        my ($cl, $dmid, $clid) = @_;
        return if $cl->{_loaded};
        delete $singleton{$dmid}{$clid};
    });

    $last_load = $now;
}

# enumerates all loaded singletons (without reloading/checked caches),
# calling the given subref with (MogileFS::Class, $dmid, $classid)
sub _foreach_singleton {
    my ($pkg, $cb) = @_;
    foreach my $dmid (keys %singleton) {
        foreach my $clid (keys %{$singleton{$dmid}}) {
            $cb->($singleton{$dmid}{$clid}, $dmid, $clid);
        }
    }
}

# enumerates all classes, (reloading if needed), calling the given
# subref with (MogileFS::Class, $dmid, $classid)
sub foreach {
    my ($pkg, $cb) = @_;
    $pkg->check_cache;
    $pkg->_foreach_singleton($cb);
}

sub class_name {
    my ($pkg, $dmid, $classid) = @_;
    my $cls = $pkg->of_dmid_classid($dmid, $classid)
        or return undef;
    return $cls->name;
}

sub class_id {
    my ($pkg, $dmid, $classname) = @_;
    return undef unless $dmid > 0 && length $classname;
    # tries to get it first from cache, then reloads and tries again.
    my $get = sub {
        foreach my $cl ($pkg->classes_of_domain($dmid)) {
            return $cl->classid if $cl->name eq $classname;
        }
        return undef;
    };
    my $id = $get->();
    return $id if $id;
    MogileFS::Class->reload_classes;
    return $get->();
}

sub classes_of_domain {
    my ($pkg, $doma) = @_;
    my $dmid = ref $doma ? $doma->id : $doma;
    $pkg->check_cache;
    return () unless $dmid && $singleton{$dmid};
    return values %{ $singleton{$dmid} };
}

# throws 'dup' on duplicate name, returns class otherwise
sub create_class {
    my ($pkg, $dom, $clname) = @_;
    my $clid = Mgd::get_store()->create_class($dom->id, $clname);
    return $pkg->of_dmid_classid($dom->id, $clid);
}

# --------------------------------------------------------------------------
# Instance methods:
# --------------------------------------------------------------------------

sub domainid     { $_[0]{dmid} }
sub classid      { $_[0]{classid} }
sub mindevcount  { $_[0]{mindevcount} }
sub policy_class { $_[0]{replpolicy} || "MogileFS::ReplicationPolicy::MultipleHosts" }
sub name         { $_[0]{name} }

sub domain {
    my $self = shift;
    return MogileFS::Domain->of_dmid($self->domainid);
}

# throws 'dup' (for name conflict), returns 1 otherwise
sub set_name {
    my ($self, $name) = @_;
    return 1 if $self->name eq $name;
    Mgd::get_store()->update_class_name(dmid      => $self->domainid,
                                        classid   => $self->classid,
                                        classname => $name);
    $self->{name} = $name;
    MogileFS::Class->invalidate_cache;
    return 1;
}

sub set_mindevcount {
    my ($self, $n) = @_;
    return 1 if $self->mindevcount == $n;
    Mgd::get_store()->update_class_mindevcount(dmid        => $self->domainid,
                                               classid     => $self->classid,
                                               mindevcount => $n);
    $self->{mindevcount} = $n;
    MogileFS::Class->invalidate_cache;
    return 1;
}

# throws:
#   'has_files'
sub delete {
    my $self = shift;
    throw("has_files") if $self->has_files;
    Mgd::get_store()->delete_class($self->domainid, $self->classid);
    MogileFS::Class->invalidate_cache;
    return 1;
}

sub has_files {
    my $self = shift;
    return Mgd::get_store()->class_has_files($self->domainid, $self->classid);
}

1;