#!/usr/bin/perl
#

use strict;
use lib "$ENV{'LJHOME'}/cgi-bin";
use LJ::Cache;

package LJ::Lang;

my @day_short   = (qw[Sun Mon Tue Wed Thu Fri Sat]);
my @day_long    = (qw[Sunday Monday Tuesday Wednesday Thursday Friday Saturday]);
my @month_short = (qw[Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec]);
my @month_long  = (qw[January February March April May June July August September October November December]);

# get entire array of days and months
sub day_list_short   { return @LJ::Lang::day_short;   }
sub day_list_long    { return @LJ::Lang::day_long;    }
sub month_list_short { return @LJ::Lang::month_short; }
sub month_list_long  { return @LJ::Lang::month_long;  }

# access individual day or month given integer
sub day_short   { return   $day_short[$_[0] - 1]; }
sub day_long    { return    $day_long[$_[0] - 1]; }
sub month_short { return $month_short[$_[0] - 1]; }
sub month_long  { return  $month_long[$_[0] - 1]; }

# lang codes for individual day or month given integer
sub day_short_langcode   { return "date.day."   . lc(LJ::Lang::day_long(@_))    . ".short"; }
sub day_long_langcode    { return "date.day."   . lc(LJ::Lang::day_long(@_))    . ".long";  }
sub month_short_langcode { return "date.month." . lc(LJ::Lang::month_long(@_))  . ".short"; }
sub month_long_langcode  { return "date.month." . lc(LJ::Lang::month_long(@_))  . ".long";  }

## ordinal suffix
sub day_ord {
    my $day = shift;

    # teens all end in 'th'
    if ($day =~ /1\d$/) { return "th"; }
        
    # otherwise endings in 1, 2, 3 are special
    if ($day % 10 == 1) { return "st"; }
    if ($day % 10 == 2) { return "nd"; }
    if ($day % 10 == 3) { return "rd"; }

    # everything else (0,4-9) end in "th"
    return "th";
}

sub time_format
{
    my ($hours, $h, $m, $formatstring) = @_;

    if ($formatstring eq "short") {
        if ($hours == 12) {
            my $ret;
            my $ap = "a";
            if ($h == 0) { $ret .= "12"; }
            elsif ($h < 12) { $ret .= ($h+0); }
            elsif ($h == 12) { $ret .= ($h+0); $ap = "p"; }
            else { $ret .= ($h-12); $ap = "p"; }
            $ret .= sprintf(":%02d$ap", $m);
            return $ret;
        } elsif ($hours == 24) {
            return sprintf("%02d:%02d", $h, $m);
        }
    }
    return "";
}

#### ml_ stuff:
my $LS_CACHED = 0;
my %DM_ID = ();     # id -> { type, args, dmid, langs => { => 1, => 0, => 1 } }
my %DM_UNIQ = ();   # "$type/$args" => ^^^
my %LN_ID = ();     # id -> { ..., ..., 'children' => [ $ids, .. ] }
my %LN_CODE = ();   # $code -> ^^^^
my $LAST_ERROR;
my $TXT_CACHE;      # LJ::Cache for text

sub get_cache_object { return $TXT_CACHE; }

sub last_error
{
    return $LAST_ERROR;
}

sub set_error
{
    $LAST_ERROR = $_[0];
    return 0;
}

sub get_lang
{
    my $code = shift;
    load_lang_struct() unless $LS_CACHED;
    return $LN_CODE{$code};
}

sub get_lang_id
{
    my $id = shift;
    load_lang_struct() unless $LS_CACHED;
    return $LN_ID{$id};
}

sub get_dom
{
    my $dmcode = shift;
    load_lang_struct() unless $LS_CACHED;
    return $DM_UNIQ{$dmcode};
}

sub get_dom_id
{
    my $dmid = shift;
    load_lang_struct() unless $LS_CACHED;
    return $DM_ID{$dmid};
}

sub get_domains
{
    load_lang_struct() unless $LS_CACHED;
    return values %DM_ID;
}

sub get_root_lang
{
    my $dom = shift;  # from, say, get_dom
    return undef unless ref $dom eq "HASH";
    foreach (keys %{$dom->{'langs'}}) {
        if ($dom->{'langs'}->{$_}) {
            return get_lang_id($_);
        }
    }
    return undef;
}

sub load_lang_struct
{
    return 1 if $LS_CACHED;
    my $dbr = LJ::get_db_reader();
    return set_error("No database available") unless $dbr;
    my $sth;

    $TXT_CACHE = new LJ::Cache { 'maxbytes' => $LJ::LANG_CACHE_BYTES || 50_000 };

    $sth = $dbr->prepare("SELECT dmid, type, args FROM ml_domains");
    $sth->execute;
    while (my ($dmid, $type, $args) = $sth->fetchrow_array) {
        my $uniq = $args ? "$type/$args" : $type;
        $DM_UNIQ{$uniq} = $DM_ID{$dmid} = { 
            'type' => $type, 'args' => $args, 'dmid' => $dmid,
            'uniq' => $uniq,
        };
    }

    $sth = $dbr->prepare("SELECT lnid, lncode, lnname, parenttype, parentlnid FROM ml_langs");
    $sth->execute;
    while (my ($id, $code, $name, $ptype, $pid) = $sth->fetchrow_array) {
        $LN_ID{$id} = $LN_CODE{$code} = {
            'lnid' => $id,
            'lncode' => $code,
            'lnname' => $name,
            'parenttype' => $ptype,
            'parentlnid' => $pid,
        };
    }
    foreach (values %LN_CODE) {
        next unless $_->{'parentlnid'};
        push @{$LN_ID{$_->{'parentlnid'}}->{'children'}}, $_->{'lnid'};
    }
    
    $sth = $dbr->prepare("SELECT lnid, dmid, dmmaster FROM ml_langdomains");
    $sth->execute;
    while (my ($lnid, $dmid, $dmmaster) = $sth->fetchrow_array) {
        $DM_ID{$dmid}->{'langs'}->{$lnid} = $dmmaster;
    }
    
    $LS_CACHED = 1;
}

sub get_itemid
{
    &LJ::nodb;
    my ($dmid, $itcode, $opts) = @_;
    load_lang_struct() unless $LS_CACHED;

    my $dbr = LJ::get_db_reader();
    $dmid += 0;
    my $itid = $dbr->selectrow_array("SELECT itid FROM ml_items WHERE dmid=$dmid AND itcode=?", undef, $itcode);
    return $itid if defined $itid;

    my $dbh = LJ::get_db_writer();
    return 0 unless $dbh;
    
    # allocate a new id
    LJ::get_lock($dbh, 'global', 'mlitem_dmid') || return 0;
    $itid = $dbh->selectrow_array("SELECT MAX(itid)+1 FROM ml_items WHERE dmid=?", undef, $dmid);
    $itid ||= 1; # if the table is empty, NULL+1 == NULL
    $dbh->do("INSERT INTO ml_items (dmid, itid, itcode, notes) ".
             "VALUES (?, ?, ?, ?)", undef, $dmid, $itid, $itcode, $opts->{'notes'});
    LJ::release_lock($dbh, 'global', 'mlitem_dmid');

    if ($dbh->err) {
        return $dbh->selectrow_array("SELECT itid FROM ml_items WHERE dmid=$dmid AND itcode=?",
                                     undef, $itcode);
    }
    return $itid;
}

sub set_text
{
    &LJ::nodb;
    my ($dmid, $lncode, $itcode, $text, $opts) = @_;
    load_lang_struct() unless $LS_CACHED;

    my $l = $LN_CODE{$lncode} or return set_error("Language not defined.");
    my $lnid = $l->{'lnid'};
    $dmid += 0;

    # is this domain/language request even possible?
    return set_error("Bogus domain") 
        unless exists $DM_ID{$dmid};
    return set_error("Bogus lang for that domain") 
        unless exists $DM_ID{$dmid}->{'langs'}->{$lnid};

    my $itid = get_itemid($dmid, $itcode, { 'notes' => $opts->{'notes'}});
    return set_error("Couldn't allocate itid.") unless $itid;

    my $dbh = LJ::get_db_writer();
    my $txtid = 0;
    if (defined $text) {
        my $userid = $opts->{'userid'} + 0;
        # Strip bad characters
        $text =~ s/\r//;
        my $qtext = $dbh->quote($text);
        LJ::get_lock( $dbh, 'global', 'ml_text_txtid' ) || return 0;
        $txtid = $dbh->selectrow_array("SELECT MAX(txtid)+1 FROM ml_text WHERE dmid=?", undef, $dmid);
        $txtid ||= 1;
        $dbh->do("INSERT INTO ml_text (dmid, txtid, lnid, itid, text, userid) ".
                 "VALUES ($dmid, $txtid, $lnid, $itid, $qtext, $userid)");
        LJ::release_lock( $dbh, 'global', 'ml_text_txtid' );
        return set_error("Error inserting ml_text: ".$dbh->errstr) if $dbh->err;
    }
    if ($opts->{'txtid'}) {
        $txtid = $opts->{'txtid'}+0;
    }

    my $staleness = $opts->{'staleness'}+0;
    $dbh->do("REPLACE INTO ml_latest (lnid, dmid, itid, txtid, chgtime, staleness) ".
             "VALUES ($lnid, $dmid, $itid, $txtid, NOW(), $staleness)");
    return set_error("Error inserting ml_latest: ".$dbh->errstr) if $dbh->err;
    LJ::MemCache::set("ml.${lncode}.${dmid}.${itcode}", $text) if defined $text;

    {
        my $vals;
        my $langids;
        my $rec = sub {
            my $l = shift;
            my $rec = shift;
            foreach my $cid (@{$l->{'children'}}) {
                my $clid = $LN_ID{$cid};
                if ($opts->{'childrenlatest'}) {
                    my $stale = $clid->{'parenttype'} eq "diff" ? 3 : 0;
                    $vals .= "," if $vals;
                    $vals .= "($cid, $dmid, $itid, $txtid, NOW(), $stale)";
                }
                $langids .= "," if $langids;
                $langids .= $cid+0;
                LJ::MemCache::delete("ml.$clid->{'lncode'}.${dmid}.${itcode}");
                $rec->($clid, $rec);
            }
        };
        $rec->($l, $rec);

        # set descendants to use this mapping
        $dbh->do("INSERT IGNORE INTO ml_latest (lnid, dmid, itid, txtid, chgtime, staleness) ".
                 "VALUES $vals") if $vals;

        # update languages that have no translation yet
        $dbh->do("UPDATE ml_latest SET txtid=$txtid WHERE dmid=$dmid ".
                 "AND lnid IN ($langids) AND itid=$itid AND staleness >= 3") if $langids;
    }

    if ($opts->{'changeseverity'} && $l->{'children'} && @{$l->{'children'}}) {
        my $in = join(",", @{$l->{'children'}});
        my $newstale = $opts->{'changeseverity'} == 2 ? 2 : 1;
        $dbh->do("UPDATE ml_latest SET staleness=$newstale WHERE lnid IN ($in) AND ".
                 "dmid=$dmid AND itid=$itid AND txtid<>$txtid AND staleness < $newstale");
    }

    return 1;
}

sub get_text
{
    my ($lang, $code, $dmid, $vars) = @_;
    $dmid = int($dmid || 1);
    $lang ||= $LJ::DEFAULT_LANG;
    load_lang_struct() unless $LS_CACHED;
    my $cache_key = "ml.${lang}.${dmid}.${code}";
    
    my $text = $TXT_CACHE->get($cache_key);

    unless (defined $text) {
        my $mem_good = 1;
        $text = LJ::MemCache::get($cache_key);
        unless (defined $text) {
            $mem_good = 0;
            my $l = $LN_CODE{$lang} or return "?lang?";
            my $dbr = LJ::get_db_reader();
            $text = $dbr->selectrow_array("SELECT t.text".
                                          "  FROM ml_text t, ml_latest l, ml_items i".
                                          " WHERE t.dmid=$dmid AND t.txtid=l.txtid".
                                          "   AND l.dmid=$dmid AND l.lnid=$l->{lnid} AND l.itid=i.itid".
                                          "   AND i.dmid=$dmid AND i.itcode=?", undef,
                                          $code);
        }
        if (defined $text) {
            $TXT_CACHE->set($cache_key, $text);
            LJ::MemCache::set($cache_key, $text) unless $mem_good;
        }
    }

    if ($vars) {
        $text =~ s/\[\[\?([\w\-]+)\|(.+?)\]\]/resolve_plural($lang, $vars, $1, $2)/eg;
        $text =~ s/\[\[([^\[]+?)\]\]/$vars->{$1}/g;
    }

    return $text;
}

# The translation system now supports the ability to add multiple plural forms of the word
# given different rules in a languge.  This functionality is much like the plural support
# in the S2 styles code.  To use this code you must use the BML::ml function and pass
# the number of items as one of the variables.  To make sure that you are allowing the
# utmost compatibility for each language you should not hardcode the placement of the
# number of items in relation to the noun.  Let the translation string do this for you.
# A translation string is in the format of, with num being the variable storing the
# number of items.
# =[[num]] [[?num|singular|plural1|plural2|pluralx]]

sub resolve_plural {
    my ($lang, $vars, $varname, $wordlist) = @_;
    my $count = $vars->{$varname};
    my @wlist = split(/\|/, $wordlist);
    my $plural_form = plural_form($lang, $count);
    return $wlist[$plural_form];
}

# TODO: make this faster, using AUTOLOAD and symbol tables pointing to dynamically
# generated subs which only use $_[0] for $count.
sub plural_form {
    my ($lang, $count) = @_;
    return plural_form_en($count) if $lang =~ /^en/;
    return plural_form_ru($count) if $lang =~ /^ru/ || $lang =~ /^uk/;
    return plural_form_fr($count) if $lang =~ /^fr/ || $lang =~ /^pt_BR/;
    return plural_form_lt($count) if $lang =~ /^lt/;
    return plural_form_pl($count) if $lang =~ /^pl/;
    return plural_form_singular() if $lang =~ /^hu/ || $lang =~ /^ja/ || $lang =~ /^tr/;
    return plural_form_lv($count) if $lang =~ /^lv/;
    return plural_form_en($count);  # default
}

# English, Danish, German, Norwegian, Swedish, Estonian, Finnish, Greek, Hebrew, Italian, Portugese, Spanish, Esperanto
sub plural_form_en {
    my ($count) = shift;
    return 0 if $count == 1;
    return 1;
}

# French, Brazilian Portuguese
sub plural_form_fr {
    my ($count) = shift;
    return 1 if $count > 1;
    return 0;
}

# Croatian, Czech, Russian, Slovak, Ukrainian
sub plural_form_ru {
    my ($count) = shift;
    return 0 if ($count%10 == 1 and $count%100 != 11);
    return 1 if ($count%10 >= 2 and $count%10 <= 4 and ($count%100 < 10 or $count%100>=20));
    return 2;
}

# Polish
sub plural_form_pl {
    my ($count) = shift;
    return 0 if($count == 1);
    return 1 if($count%10 >= 2 && $count%10 <= 4 && ($count%100 < 10 || $count%100 >= 20));
    return 2;
}

# Lithuanian
sub plural_form_lt {
    my ($count) = shift;
    return 0 if($count%10 == 1 && $count%100 != 11);
    return 1 if ($count%10 >= 2 && ($count%100 < 10 || $count%100 >= 20));
    return 2;
}

# Hungarian, Japanese, Korean (not supported), Turkish
sub plural_form_singular {
    return 0;
}

# Latvian
sub plural_form_lv {
    my ($count) = shift;
    return 0 if($count%10 == 1 && $count%100 != 11);
    return 1 if($count != 0);
    return 2;
}

1;
