# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (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.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# Contributor(s): Jochen Wiedmann <jochen.wiedmann@gmail.com>

use strict;

use Bugzilla();

package Bugzilla::Enum;

sub items($) {
    my $self = shift;
    my $items = $self->{items};
    if (!$items) {
        $self->{items} = $items = $self->initItems();
    }
    return $items;
}

sub names($;$) {
    my($self, $items) = @_;
    $items = $self->items() unless $items;
    my @names = map { $_->name() } @$items;
    return \@names;
}

sub nums($;$) {
    my($self, $items) = @_;
    $items = $self->items() unless $items;
    my @nums = map { $_->num() } @$items;
    return \@nums;
}
    
sub by_name($$;$) {
    my($self, $name, $exc) = @_;
    if (defined($name)) {
        foreach my $item (@{$self->items()}) {
            if ($item->name() eq $name) {
                return $item;
            }
        }
    }
    if (defined($exc) && !$exc) {
        return undef;
    }
    $name = "undef" unless defined $name;
    die "Unknown item name: $name";
}

sub by_num($$;$) {
    my($self, $num, $exc) = @_;
    if (defined($num)) {
        foreach my $item (@{$self->items()}) {
            if ($item->num() eq $num) {
                return $item;
            }
        }
    }
    if (defined($exc) && !$exc) {
        return undef;
    }
    $num = "undef" unless defined $num;
    die "Unknown item value: $num";
}

sub active_items($;$) {
    my($self, $items) = @_;
    $items = $self->items() unless $items;
    my @result = grep { $_->is_active() } @$items;
    return \@result;
}

sub active_names($) {
    my $self = shift;
    return $self->names($self->activeItems());
}

sub active_nums($) {
    my $self = shift;
    return $self->nums($self->activeItems());
}

package Bugzilla::Enum::Builtin;

our @ISA = qw(Bugzilla::Enum);

sub new($$) {
    my($class, $table) = @_;
    my $self = {table => $table};
    bless($self, (ref($class) || $class));
    return $self;
}

sub initItems($) {
    my $self = shift;
    my @items;
    my $dbh = Bugzilla->dbh;
    my $sth = $dbh->prepare("SELECT * FROM custom_enum ORDER BY sortkey, value");
    $sth->execute();
    while (defined(my $values = $sth->fetchrow_hashref())) {
        my $num = $values->{id};
        my $name = $values->{value};
        push(@items, Bugzilla::Enum::Item->new($num, $name, $values));
    }
    $sth->finish();
    return \@items;
}

sub table($) {
    return shift->{'table'};
}

package Bugzilla::Enum::OpSys;

our @ISA = qw(Bugzilla::Enum::Builtin);

package Bugzilla::Enum::RepPlatform;

our @ISA = qw(Bugzilla::Enum::Builtin);

package Bugzilla::Enum::Resolution;

our @ISA = qw(Bugzilla::Enum::Builtin);

package Bugzilla::Enum::BugSeverity;

our @ISA = qw(Bugzilla::Enum::Builtin);

package Bugzilla::Enum::BugStatus;

our @ISA = qw(Bugzilla::Enum::Builtin);

package Bugzilla::Enum::Priority;

our @ISA = qw(Bugzilla::Enum::Builtin);

package Bugzilla::Enum::Item;

sub new($$$$) {
    my($class, $num, $name, $values) = @_;
    die "Missing item name" unless defined($name);
    die "Missing numeric value" unless defined($num);
    if ($values) {
        # Create a copy of the values hash, because DBI might reuse
        # the same hash ref again.
        my %vals = %$values;
        $values = \%vals;
    } else {
        $values = {};
    }
    my $self = { num => $num, name => $name, values => $values };
    bless($self, ref($class) || $class);
    return $self;
}

sub num($) {
    return shift->{num};
}

sub name($) {
    return shift->{name};
}

sub get($$) {
    my($self, $name) = @_;
    return $self->{values}->{$name};
}

sub sortkey($) {
    return shift->{'sortkey'};
}

sub is_active($) {
    return shift->{'isactive'};
}

package Bugzilla::Enum;

our $OP_SYS = Bugzilla::Enum::OpSys->new("op_sys");
our $REP_PLATFORM = Bugzilla::Enum::RepPlatform->new("rep_platform");
our $RESOLUTION = Bugzilla::Enum::Resolution->new("resolution");
our $BUG_SEVERITY = Bugzilla::Enum::BugSeverity->new("bug_severity");
our $BUG_STATUS = Bugzilla::Enum::BugStatus->new("bug_status");
our $PRIORITY = Bugzilla::Enum::Priority->new("priority");

package Bugzilla::Enum::Custom;

our @ISA = qw(Bugzilla::Enum);
our @instances;

sub new($$) {
    my($class, $enum_id) = @_;
    my $self = {enum_id => $enum_id};
    bless($self, (ref($class) || $class));
    return $self;
}

sub initItems($) {
    my $self = shift;
    my @items;
    my $dbh = Bugzilla->dbh;
    my $sth = $dbh->prepare("SELECT * FROM " . $self->table()
                            . " ORDER BY sortkey, value");
    $sth->execute();
    while (defined(my $values = $sth->fetchrow_hashref())) {
        my $num = $values->{id};
        my $name = $values->{value};
        push(@items, Bugzilla::Enum::Item->new($num, $name, $values));
    }
    $sth->finish();
    return \@items;
}

sub instance($$) {
    my($class, $item) = @_;
    my $result = $instances[$item];
    if (!$result) {
        $result = $class->new($item);
        $instances[$item] = $result;
    }
    return $result;
}

1;

__END__

=pod

=head1 NAME

Bugzilla::Enum - Implementation of database based enumerations

=head1 SYNOPSIS

  use Bugzilla::Enum;

  # Get access to the builtin enumeration op_sys
  my $op_sys = Bugzilla::Enum::OP_SYS;
  # Get access to a custom enumeration with ID 5
  my $custom_enum_5 = Bugzilla::Enum::Custom->instance(5);

  # Get the enumeration item with the name "Windows 2000"
  my $val1 = $op_sys->by_name("Windows 2000");
  # Get the enumeration item with the numeric value 2
  my $val2 = $op_sys->by_num(2);
  # Get all items in the enumeration, ordered by the sort key
  my $items = $op_sys->items();
  # Get all names in the enumeration, ordered by the sort key
  my $names = $op_sys->names();
  # Get all numbers in the enumeration, ordered by the sort key
  my $nums = $op_sys->nums();

  # Print the numeric value
  print $val1->num();
  # Print the human readable value
  print $val2->name();


=head1 DESCRIPTION

This package provides access to enumerations, which are stored
in the database. An enumeration is defined as a set of rows in
a common table. The rows are called the "enumeration items".
Any item in the enumeration has a numeric value (which is
typically being used within foreign keys referencing the
enumeration) and a human readable value, the name. Both the
numeric value and the name are unique within the enumeration.

For example, the op_sys enumeration consists of all rows in
the table op_sys. The numeric values are given by the column
"id". The names are given by the column "value".

The op_sys enumeration is an example of an enumeration, which
is built into Bugzilla. Other builtin enumerations include
the priorities, the bug resolutions, and so on. However, an
enumeration may also be defined by the user. For example, a
custom field implementation may allow the user to select one
value from a predefined set.

Besides the builtin enumerations, there are also custom
enumerations. Custom enumerations are stored in the table
C<custom_enum> and identified by a common key, the C<enum_id>,
which is unique per custom enumerations.

=head1 BUILTIN ENUMERATIONS

Several enumerations are built into Bugzilla:

=over

=item $Bugzilla::Enum::OP_SYS

An instance of L<Bugzilla::Enum::OpSys>, providing access
to the table C<op_sys>.

=item $Bugzilla::Enum::REP_PLATFORM

An instance of L<Bugzilla::Enum::RepPlatform>, providing
access to the table C<rep_platform>.

=item $Bugzilla::Enum::RESOLUTION

An instance of L<Bugzilla::Enum::Resolution>, providing
access to the table C<resolution>.

=item $Bugzilla::Enum::BUG_SEVERITY

An instance of L<Bugzilla::Enum::BugSeverity>, providing
access to the table C<bug_severity>.

=item $Bugzilla::Enum::BUG_STATUS

An instance of L<Bugzilla::Enum::BugStatus>, providing
access to the table C<bug_status>.

=item $Bugzilla::Enum::PRIORITY

An instance of L<Bugzilla::Enum::Priority>, providing
access to the table C<priority>.

=back

=head1 FUNCTIONS

The following methods are available:

=head2 Accessing an enumeration item by name

  my $item = $enum->by_name($name);
  my $item = $enum->by_name($name, $exc);

Returns the enumeration item named C<$name>. The first version
throws an exception, if the name C<$name> is unknown. The latter
version allows to turn exceptions off: If the parameter C<$exc>
is false, then an unknown or undefined name will be signalled
by returning the value undef.

Enumeration items are instances of C<Bugzilla::Enum::Item>.

=head2 Accessing an enumeration value by number

  my $item = $enum->by_num($num);
  my $item = $enum->by_num($num, $exc);

Returns the enumeration item with the numeric value C<$num>.
The first version throws an exception, if the name C<$num> is
unknown. The latter version allows to turn exceptions off: If
the parameter C<$exc> is false, then an unknown or undefined
number will be signalled by returning the value undef.

Enumeration items are instances of C<Bugzilla::Enum::Item>.

=head2 Accessing an enumeration items numeric value

  my $num = $item->num();

The method C<num> returns an items numeric value.

=head2 Accessing an enumeration items name

  my $name = $item->name();

The method C<name> returns an items name.

=head2 Accessing an enumeration items hash set.

  my $description = $item->get('description');

Enumeration items may have additional attributes. For example,
the op_sys enumeration has attributes 'id' (which is, in fact,
the numeric value), 'value' (which is, in fact, the name),
'sortkey', and 'isactive'. The methods C<num> and C<name>
provide access to the first two attributes. The other
attributes can be accessed via the C<get> method. The method
parameter is the respective column name.

In the case of the op_sys enumeration, the numeric value may
be accessed via either of

  my $num = $item->num();
  my $num = $item->get('id');

In general, however, you should not assume, that an items
name or numeric value may be accessed via the C<get> method.

=head2 Getting all items of an enumeration

  my $items = $enum->items();

Returns a reference to an ordered list with the enumerations
items. In the case of builtin and custom enumerations, the
order is by C<sortkey>.

=head2 Getting all active items of an enumeration

  my $items = $enum->active_items();

Like C<items()>, except that only active items are being
returned.

=head2 Getting all names of an enumeration

  my $names = $enum->names();

Returns a reference to an ordered list with the names of the
enumerations items. In the case of builtin and custom enumerations,
the order is by C<sortkey>.

=head2 Getting all active names of an enumeration

  my $items = $enum->active_names();

Like C<names()>, except that only active names are being
returned.

=head2 Getting all numbers of an enumeration

  my $nums = $enum->nums();

Returns a reference to an ordered list with the numbers of the
enumerations items. In the case of builtin and custom enumerations,
the order is by C<sortkey>.

=head2 Getting all active numbers of an enumeration

  my $nums = $enum->active_nums();

Like C<nums()>, except that only active names are being
returned.

=cut
