#!/usr/bin/perl
use strict;

use Data::Dumper;
use lib '/app/tools/common/lib';
use lib '/app/tools/rps/lib';

use Getopt::Std;
use POSIX qw(ceil);
use Common::UTF8;

use Common::RSApp;
use Common::RSDB;

use constant kAscii => 1;
use constant kLatin1 => 2;
use constant kUTF8 => 3;

my %gTableCharset;

my $gVerbosityLevel = 1;
my $gCommitChanges = 0;

binmode(STDOUT, ":utf8");

my %options;
_parseCommandLine(\%options);
my $clientID = $options{clientID};

#foreach my $clientID (_getLocalClientIDs())
#{
    eval { _checkDB($clientID); };
    print $@ . "\n" if $@;
    print "\n\n";
#}

sub _parseCommandLine
{
    my ($settings) = @_;

    my %opt;
    getopts('c:V:g', \%opt);

    if (! $opt{c})
    {
        _usage();
        exit(1);
    }
    $settings->{clientID} = $opt{c};

    if (defined $opt{V})
    {
        $gVerbosityLevel = $opt{V};
    }

    if (defined $opt{g})
    {
        $gCommitChanges = 1;
    }
}

sub _usage
{
    print "\nusage: $0 -c <client_id> [-g]\n";
    print "\n";
    print "Arguments:\n";
    print "\t-c <client_id>\t\tThe client_id of the client to process\n";
    print "\t-g \t\t\tThe 'go flag' - If this is not specified, no changes will be committed\n";
}


sub _checkDB
{
    my ($clientID) = @_;

    _report("");
    _report("------------");
    _report("client $clientID " . Common::RSDB::ClientIDToDBName($clientID));

    # TURN OFF the 'auto-utf8' mechanism.
    #
    my $app = Common::RSApp->new(clientID => $clientID, useRSDBI => 0);
#    my $app = Common::RSApp->new(clientID => $clientID);

    my $dbo = Common::RSApp::GetClientDB();


    # Alter artist and composer to remove the unique constraint on 'name'
    #
    if ($gCommitChanges)
    {
    eval {
        $dbo->DoCmd("ALTER TABLE artist DROP KEY `artist_name`");
    };

    eval {
        $dbo->DoCmd("ALTER TABLE composer DROP KEY `name`");
    };
    }

    #  look at every table.
    #
    my @allTables = _getAllTables($dbo);


    # Get the encoding of each table.
    #
    my $sth = $dbo->DoCmd('show table status');
    while (my $hr = $sth->fetchrow_hashref())
    {
        my $tableName = $hr->{Name};
        my $collation = $hr->{Collation};
        my $encoding = kLatin1;
        if ($collation =~ /utf8/)
        {
            $encoding = kUTF8;
        }
        $gTableCharset{$tableName} = $encoding;
    }

    foreach my $table (@allTables)
    {
        _convertTable($dbo, $table, $clientID);
    }
}

sub _convertTable
{
    my ($dbo, $tableName, $clientID) = @_;


    # What we'll do is scan the table, and collect a sequence
    # of alter statements.
    #
    # Then we'll convert the data in place back to latin1 if necessary.
    #
    # Finally, we'll execute those alters.
    #
    my @alters;

    my $hasPrimaryKey = 0;
    my @keyFields;

    _report("scanning $tableName", 2);

    # First I'll take a look at all the indexes.
    # We'll need to know if any of the TEXT fields are FULLTEXT INDEX.
    #
    my %indexData;
    my $sth = $dbo->DoCmd("show index from $tableName");
    my $ar = $sth->fetchall_arrayref();
    if ($ar)
    {
        foreach my $indexDesc (@$ar)
        {
            $indexData{$indexDesc->[4]} = {
                keyName => $indexDesc->[2],
                type => $indexDesc->[10],
            };
        }
    }


    # Alter the table encoding if necessary
    #
    if (kUTF8 != $gTableCharset{$tableName})
    {
        push @alters, "ALTER TABLE $tableName CHARACTER SET utf8";
    }


    # get the schema, determine which are text fields
    #
    $sth = $dbo->DoCmd("desc $tableName");
    $ar = $sth->fetchall_arrayref();

    my @textFields;
    foreach my $fieldDesc (@$ar)
    {
        if ($fieldDesc->[1] =~ /varchar\((.*)\)/)
        {
            my $num = $1;
            push @textFields, $fieldDesc->[0];

            push @alters, "ALTER TABLE $tableName MODIFY " . $fieldDesc->[0] . " VARCHAR($num) CHARACTER SET utf8" if (kUTF8 != $gTableCharset{$tableName});
        }
        elsif ($fieldDesc->[1] =~ /text/)
        {
            push @textFields, $fieldDesc->[0];

            push @alters, "ALTER TABLE $tableName MODIFY " . $fieldDesc->[0] . " TEXT CHARACTER SET utf8" if (kUTF8 != $gTableCharset{$tableName});
        }

        if ('PRI' eq $fieldDesc->[3])
        {
            $hasPrimaryKey = 1;
            $#keyFields = 0;
            $keyFields[0] = $fieldDesc->[0];
        }
        
        if (! $hasPrimaryKey && 'MUL' eq $fieldDesc->[3])
        {
            push @keyFields, $fieldDesc->[0];
        }
    }

#    return unless scalar @textFields;


    # Now we scan for high-order characters.
    #
    $sth = $dbo->DoCmd("SELECT * FROM $tableName");
    while (my $hr = $sth->fetchrow_hashref())
    {
        foreach my $fieldToCheck (@textFields)
        {
            my $textToCheck = $hr->{$fieldToCheck};
            if (defined $textToCheck)
            {
                my $textChanged = 0;
                my $encoding = describeString($textToCheck);
                if (kUTF8 == $encoding && $gTableCharset{$tableName} == kLatin1)
                {
                    # Attempt to convert back to latin1.
                    # !!! This could fail !!!
                    #
                    _report("   ...downgrading field to latin1", 3);
                    if (! utf8::downgrade($textToCheck, 1))
                    {
                        _report("ERROR!!! Unable to convert string back to latin1 for this record: " . Dumper($hr), 3);
                        next;
                    }
                    $textChanged = 1;
                }
                elsif (kLatin1 == $encoding && $gTableCharset{$tableName} == kUTF8)
                {
                    # This always works...
                    # 
                    _report("   ...upgrading to utf8", 3);
                    utf8::upgrade($textToCheck);
                    $textChanged = 1;
                }

                if ($textChanged)
                {
                    # Update the record with the new data.
                    #
                    my @keys;
                    foreach my $keyField (@keyFields)
                    {
                        push @keys, "$keyField=".$dbo->DBQuote($hr->{$keyField});
                    }

                    my $sql = "UPDATE $tableName SET $fieldToCheck=".$dbo->DBQuote($textToCheck)." WHERE " . join(' AND ', @keys);
                    _report("SQL: $sql", 3);
                    $dbo->DoCmd($sql) if $gCommitChanges;
                }
            }
        }
    }


    # now apply any alters, if necessary
    #
    foreach my $alter (@alters)
    {
        _report("   APPLYING THIS ALTER: $alter", 2);
        $dbo->DoCmd($alter) if $gCommitChanges;
    }
}

sub _getAllTables
{
    my ($dbo) = @_;

    my @allTables;
    my $sth = $dbo->DoCmd('show tables');
    while (my $ar = $sth->fetchrow_arrayref())
    {
        push @allTables, $ar->[0];
    }

    return @allTables;
}


sub _getLocalClientIDs
{
    #return 60; # RSDEMO
#    return 68; # SIXDEGREES
#    return 23; # COMPASS

    # We have a nifty little script that will give us a list of client databases.
    #
    my $clientListString = `/app/tools/rps/bin/db/list_client_dbs.pl`;
    chomp $clientListString;
    # Find the client id that matches each database.
    #
    my @ids;
    my @dbNames = split(/ /, $clientListString);
    foreach my $dbName (@dbNames)
    {
        my $id = Common::RSDB::DBNameToClientID($dbName);
        push @ids, $id;
    }
    return @ids;
}





# There are 3 different cases that I want to capture:
# 1 - Everything in the string is low-order ascii.
# 2 - There are high-order latin1 characters.
# 3 - There are high-order UTF8 characters.
#
sub describeString
{
    my ($string) = @_;

    my @bytes = unpack("C*", $string);

    my $numBytes = scalar @bytes;
    for (my $i = 0; $i < $numBytes; $i++)
    {
        next if ($bytes[$i] < 128);

        # Check the high-order bits, to see how many multi-bytes to expect.
        #
        # We're just going to check the first (possibly) wide character we see.

        if (0xf0 == ($bytes[$i] & 0xf0))
        {
            # Possibly 4-byte
            #
            return kUTF8 if _checkUTF8Bytes(\@bytes, 4, $i);
        }
        elsif (0xe0 == ($bytes[$i] & 0xe0))
        {
            # Possibly 3-byte
            #
            return kUTF8 if _checkUTF8Bytes(\@bytes, 3, $i);
        }
        elsif (0xc0 == ($bytes[$i] & 0xc0))
        {
            # Possibly 2-byte
            #
            return kUTF8 if _checkUTF8Bytes(\@bytes, 2, $i);
        }
        else
        {
            # It ain't utf8
            #
            return kLatin1;
        }
    }

    return kAscii;
}

sub _checkUTF8Bytes
{
    my ($bytes, $order, $i) = @_;

    # Do we have enough bytes left in the string?
    #
    return 0 if ($i + $order >= (scalar @$bytes + 1));

    # All subsequent bytes should start with 0b10xxxxxx
    #
    for (my $x = 1; $x < $order; $x++)
    {
        my $j = $i + $x;
        my $byte = $bytes->[$j];

        if (0x80 != ($byte & 0xc0))
        {
            return 0;
        }
    }

    return 1;
}

sub _report
{
    my ($msg, $level) = @_;

    $level = 1 unless $level;
    if ($level <= $gVerbosityLevel)
    {
        print localtime() . " $msg\n";
    }
}
