#!/usr/bin/perl
# ----------------------------------------------------------------------------
# This software is Copyright (C) 2007, All Rights Reserved
# ----------------------------------------------------------------------------
# This program is protected by local and international copyright laws. Any
# use of this program is subject to the the terms of the license agreement
# included as part of this distribution archive. Any other uses are strictly
# prohibited without the written permission of the Vendor and all
# other rights are reserved.
# -----------------------------------------------------------------------------
# Warning : Modifying this program violates your license agreement!
# -----------------------------------------------------------------------------

package IT::DB::Schema;

use strict;

use IT::Globals;
use IT::DB::Globals;

# module globals
use vars qw($SCHEMA_CONNECTIONS $SCHEMA_TABLES $CACHED_LOOKUP_TABLES $FIELD_ATTRIBUTES);
$SCHEMA_CONNECTIONS = {};
$SCHEMA_TABLES = {};
$CACHED_LOOKUP_TABLES = {};
$FIELD_ATTRIBUTES     = {}; # aliases for _fieldDefsByName lookup table

# -----------------------------------------------------------------------------
# Function    : _updateCachedLookupTables
# Description : update _fieldDefsByName keys in $CACHED_LOOKUP_TABLES.
# Usage       : &_updateCachedLookupTables();
# -----------------------------------------------------------------------------

sub _updateCachedLookupTables {
  if (@_) { die "function takes no arguments!"; }

  # clear cached tables;
  $CACHED_LOOKUP_TABLES = {};

  foreach my $tableName (keys %{$SCHEMA_TABLES}) {
    my $fieldDefs       = $SCHEMA_TABLES->{$tableName}{'fields'};
    my %fieldDefsByName = map { $_->{'name'} => $_ } @$fieldDefs;
    $CACHED_LOOKUP_TABLES->{'fieldDefsByName'}{$tableName} = \%fieldDefsByName;
  }

  # update aliases tables
  $FIELD_ATTRIBUTES = $CACHED_LOOKUP_TABLES->{'fieldDefsByName'};

}

# -----------------------------------------------------------------------------
# Function    : loadSchema
# Description : load schema data from file into table and connection globals
# Usage       : &IT::DB::Schema::loadSchema($schemaFilePath);
# -----------------------------------------------------------------------------

sub loadSchema {
  # NOTE:  We don't call &IT::DB::_die_unless_init_called(); because IT::DB::Init calls this function!
  my $schemaFilePath = shift || &IT::Error::dieAsCaller("No schemaFilePath specified!");

  # load schema files
  &IT::FileLock::getLock('exclusive');
  my $schema = eval { &IT::FreezeThaw::loadStructFromFile($schemaFilePath, 'ALLOW_PERL_EVAL'); };
  my $evalError = $@;
  &IT::FileLock::releaseLock();

  # error checking
  if ($evalError) {
    die "Error loading schema file: $schemaFilePath!\n$evalError";
  }

  ### connection error checking
  foreach my $connectionName (keys %{$schema->{'connections'}}) {

    # check connection key and name match
    my $connectionRef = $schema->{'connections'}{$connectionName};
    if ($connectionName ne $connectionRef->{'name'}) { die "Schema error: connection name '$connectionRef->{'name'}' doesn't match table key '$connectionName' in $schemaFilePath\n"; }

    # check table attributes are valid
    foreach my $attributeName (keys %$connectionRef) {
      unless ($VALID_CONNECTION_ATTRIBUTES{$attributeName}) { die "Schema error: Invalid connection attribute '$attributeName' in $schemaFilePath\n"; }
    }

  }

  ### table error checking
  foreach my $tableName (keys %{$schema->{'tables'}}) {

    # check table key and name match
    my $tableRef = $schema->{'tables'}{$tableName};
    if ($tableName ne $tableRef->{'name'}) { die "Schema error: table name '$tableRef->{'name'}' doesn't match table key '$tableName' in $schemaFilePath\n"; }

    # check table attributes are valid
    foreach my $attributeName (keys %$tableRef) {
      unless ($VALID_TABLE_ATTRIBUTES{$attributeName}) { die "Schema error: Invalid table attribute '$attributeName' in $schemaFilePath\n"; }
    }

    # check field attributes are valid
    eval { &IT::DB::Common::dieOnFieldDefinitionErrors($tableRef->{'fields'}); };
    if ($@) { die "Schema error: $@ in $schemaFilePath\n"; }
  }

  #

  # set globals
  $SCHEMA_TABLES      = $schema->{'tables'};
  $SCHEMA_CONNECTIONS = $schema->{'connections'};
  &_updateCachedLookupTables();

  return $schema;
}


# -----------------------------------------------------------------------------
# Function    : saveSchema
# Description : save schema data to file
# Usage       : &IT::DB::Schema::saveSchema($SCHEMA_FILEPATH);
# -----------------------------------------------------------------------------

sub saveSchema {
  &IT::DB::_die_unless_init_called();
  my $schemaFilePath = shift || &IT::Error::dieAsCaller("No schemaFilePath specified!");
  if (shift) { die "saveSchema only takes one argument!"; }

  &IT::FileLock::lockRequired('exclusive');

  # copy connection schema without temp session keys
  my $connectionsWithoutTempKeys = {};
  foreach my $connectionName (keys %$SCHEMA_CONNECTIONS) {
    $connectionsWithoutTempKeys->{$connectionName} = {};
    my $connectionRef = $SCHEMA_CONNECTIONS->{$connectionName};

    foreach my $key (keys %$connectionRef) {
      if ($key =~ /^_/) { next; } # temp session keys are prefixed with _
      if (ref $connectionRef->{$key}) { die "connection key '$key' can't be a reference!"; }
      $connectionsWithoutTempKeys->{$connectionName}{$key} = $connectionRef->{$key};
    }
  }

  # save schema to file
  my $schema = {
    connections => $connectionsWithoutTempKeys,
    tables      => $SCHEMA_TABLES,
  };
  my $freezeAs = 'perl';
  &IT::FreezeThaw::saveStructToFile($schemaFilePath, $schema, $freezeAs);

  return 1;
}

# -----------------------------------------------------------------------------
# Function    : createConnection
# Description : Create a new connection in the schema and return it.
# Usage       : my $connectionRef = &IT::DB::Schema::createConnection($attributes);
# -----------------------------------------------------------------------------

sub createConnection {
  &IT::DB::_die_unless_init_called();
  my $attributes = shift || &IT::Error::dieAsCaller("No connection attributes specified!");

  # error checking
  &IT::Error::dieOnValidationErrors(
    [ "\@_", \@_, "noArgsLeft" ],
    [ "\$attr", $attributes, "hashRef hashKeysRequired(name, engine)" ],
    [ "\$attr->{'engine'}", $attributes->{'engine'}, "oneOf(virtual,file,mysql)" ],
  );

  # error checking
  foreach my $attribute (keys %$attributes) {
    unless ($VALID_CONNECTION_ATTRIBUTES{$attribute}) {
      die "setConnectionAttribute: '$attribute' isn't a valid connection attribute!";
    }
  }

  my $connectionName = $attributes->{'name'};
  if (exists $SCHEMA_CONNECTIONS->{$connectionName}) {
   &IT::Error::dieAsCaller("A connection named $connectionName already exists!");
  }

  # create new connection
  $SCHEMA_CONNECTIONS->{$connectionName} = $attributes;

  # return connection ref
  return $SCHEMA_CONNECTIONS->{$connectionName};
}

# -----------------------------------------------------------------------------
# Function    : dropConnection
# Description : remove a connection from the schema.  Dies if any tables are
#               still using this connection
# Usage       : &IT::DB::Schema::dropConnection($connectionName);
# -----------------------------------------------------------------------------

sub dropConnection {
  &IT::DB::_die_unless_init_called();
  my $connectionName = shift || &IT::Error::dieAsCaller("No connection name specified!");

  # check if connection exists
  if (!exists $SCHEMA_CONNECTIONS->{$connectionName}) {
    die "Can't drop connection '$connectionName' because it doesn't exist in schema!";
  }

  # check if any tables are still using this connection
  foreach my $tableName (&IT::DB::Schema::listTables()) {
    my $tableConnection = &IT::DB::Schema::getTableAttributes($tableName, 'connectionName');
    if ($tableConnection eq $connectionName) {
      die "Can't drop connection '$connectionName' because it's still being used by table '$tableName'!\n";
    }
  }

  delete $SCHEMA_CONNECTIONS->{$connectionName};

  return 1;
}

# -----------------------------------------------------------------------------
# Function    : renameConnection
# Description : Rename a connection in the schema and update all tables on that
#               connection to have to the new connection name.
# Usage       : &IT::DB::Schema::renameConnection($oldConnectionName, $newConnectionName);
# -----------------------------------------------------------------------------

sub renameConnection {
  &IT::DB::_die_unless_init_called();
  my $oldConnectionName = shift || &IT::Error::dieAsCaller("No old connection name specified!");
  my $newConnectionName = shift || &IT::Error::dieAsCaller("No new connection name specified!");

  if (!exists $SCHEMA_CONNECTIONS->{$oldConnectionName}) {
    die "connection '$oldConnectionName' can't be renamed because it doesn't exist in schema!";
  }
  if (exists $SCHEMA_CONNECTIONS->{$newConnectionName}) {
    die "connection '$oldConnectionName' can't be renamed because the new connection name '$newConnectionName' already exists!";
  }

  ### Update Connection in Schema
  $SCHEMA_CONNECTIONS->{$newConnectionName} = $SCHEMA_CONNECTIONS->{$oldConnectionName};   # copy old reference for new key
  $SCHEMA_CONNECTIONS->{$newConnectionName}{'name'} = $newConnectionName;                  # update name key
  delete $SCHEMA_CONNECTIONS->{$oldConnectionName};                                        # remove old key

  ### Update tables on this connection
  foreach my $tableName (&listTables()) {
    my $tableConnectionName = &getTableAttributes($tableName, 'connectionName');
    if ($tableConnectionName eq $oldConnectionName) {
      &setTableAttribute($tableName, 'connectionName', $newConnectionName);
    }
  }

  return 1;
}

# -----------------------------------------------------------------------------
# Function    : replaceConnection
# Description : Like rename connection (except with arguments in reverse order), but
#               drops the old connection and replaces
#               it with the new connection in one function. This avoids dropConnection
#               errors when you still have tables using the old Connection.  Any tables
#               pointing to the old connection will still point there, but the old connection
#               will have been replaced with the new connection data.
# Usage       : &IT::DB::Schema::replaceConnection($oldConnectionName, $newConnectionName);
# -----------------------------------------------------------------------------

sub replaceConnection {
  &IT::DB::_die_unless_init_called();
  my $oldConnectionName = shift || &IT::Error::dieAsCaller("No old connection name specified!");
  my $newConnectionName = shift || &IT::Error::dieAsCaller("No new connection name specified!");

  if (!exists $SCHEMA_CONNECTIONS->{$oldConnectionName}) {
    die "connection '$oldConnectionName' can't be replaced because it doesn't exist in schema!";
  }
  if (!exists $SCHEMA_CONNECTIONS->{$newConnectionName}) {
    die "connection '$oldConnectionName' can't be replaced because the new connection name '$newConnectionName' doesn't exists in schema!";
  }

  ### Update Connection in Schema
  $SCHEMA_CONNECTIONS->{$oldConnectionName} = $SCHEMA_CONNECTIONS->{$newConnectionName};   # copy old reference for new key
  $SCHEMA_CONNECTIONS->{$oldConnectionName}{'name'} = $oldConnectionName;                  # update name key
  delete $SCHEMA_CONNECTIONS->{$newConnectionName};                                        # remove old key

  ### Update tables on (what was) new connection
  foreach my $tableName (&listTables()) {
    my $tableConnectionName = &getTableAttributes($tableName, 'connectionName');
    if ($tableConnectionName eq $newConnectionName) {
      &setTableAttribute($tableName, 'connectionName', $oldConnectionName);
    }
  }

  return 1;
}

# -----------------------------------------------------------------------------
# Function    : listConnections
# Description : return a sorted list of connections in the schema
# Usage       : my @connections = &IT::DB::Schema::listConnections();
# -----------------------------------------------------------------------------

sub listConnections {
  &IT::DB::_die_unless_init_called();
  my @schemaConnections = sort keys %{$SCHEMA_CONNECTIONS};

  return @schemaConnections;
}

# -----------------------------------------------------------------------------
# Function    : existsConnection
# Description : check to see if a given connection exists in schema
# Usage       : if (&IT::DB::Schema::existsConnection($connectionName)) {
# -----------------------------------------------------------------------------

sub existsConnection {
  &IT::DB::_die_unless_init_called();
  my $connectionName   = shift || &IT::Error::dieAsCaller("No connection name specified!");
  my $connectionExists = $SCHEMA_CONNECTIONS->{$connectionName} ? 1 : 0;

  return $connectionExists;
}


# -----------------------------------------------------------------------------
# Function    : getConnectionAttributes
# Description : return an attribute value(s) for a given connect (eg: engine, username, etc)
# Usage       : my $attribute = &IT::DB::Schema::getConnectionAttributes($connectionName, $attributeName);
# Usage       : my ($value1, $value2, $value3) = &IT::DB::Schema::getConnectionAttributes($connectionName, $name1, $name2, $name3);
# -----------------------------------------------------------------------------

sub getConnectionAttributes {
  &IT::DB::_die_unless_init_called();
  my $connectionName = shift || &IT::Error::dieAsCaller("no connection name specified!");
  my @names          = @_;

  # error checking
  if (!@names || !defined $names[0]) {&IT::Error::dieAsCaller("No connection attribute name(s) specified!"); }

  my $connectionRef = $SCHEMA_CONNECTIONS->{$connectionName} || &IT::Error::dieAsCaller("connection '$connectionName' does not exist in schema!");

  # get list of values
  my @values;
  foreach my $name (@names) {
    unless ($VALID_CONNECTION_ATTRIBUTES{$name}) { die "getConnectionAttributes: '$name' isn't a valid connection attribute!"; }
    push @values, $connectionRef->{$name}
  }

  # return values
  if (wantarray) { return @values; }
  else           { return $values[0]; }

}


# -----------------------------------------------------------------------------
# Function    : setConnectionAttribute
# Description : set an attribute value for a given connection (eg: engine, username, etc)
# Usage       : &IT::DB::Schema::setConnectionAttribute($connectionName, $attributeName, $attributeValue);
# -----------------------------------------------------------------------------

sub setConnectionAttribute {
  &IT::DB::_die_unless_init_called();
  my $connectionName = shift || &IT::Error::dieAsCaller("no connection name specified!");
  my $name           = shift || &IT::Error::dieAsCaller("no attribute name specified!");
  my $value          = shift;

  # error checking
  unless ($VALID_CONNECTION_ATTRIBUTES{$name}) { die "setConnectionAttribute: '$name' isn't a valid connection attribute!"; }
  if ($name eq 'name')                         { die "setConnectionAttribute: can't change 'name' with setConnectionAttribute, call renameConnection instead!"; }

  # set attribute
  my $connectionRef = $SCHEMA_CONNECTIONS->{$connectionName} || &IT::Error::dieAsCaller("connection '$connectionName' does not exist in schema!");
  $connectionRef->{$name} = $value;

  return 1;
}

# -----------------------------------------------------------------------------
# Function    : createTable
# Description : Create a new table definition in the schema and returns it.
#               You should filelock and check to make sure table name doesn't
#               already exist in datastore first!
# Usage       : my $tableDef = &IT::DB::Schema::createTable($connectionName, $tableName, $fieldDefs);
# -----------------------------------------------------------------------------

sub createTable {
  &IT::DB::_die_unless_init_called();
  my $connectionName    = shift || &IT::Error::dieAsCaller("No connection name specified!");
  my $tableName         = shift || &IT::Error::dieAsCaller("No table name specified!");
  my $fieldDefsOriginal = shift || &IT::Error::dieAsCaller("No field definitions specified!");
  my $fieldDefs         = &IT::FreezeThaw::structCopy($fieldDefsOriginal); # make copy so we don't modify original (such as when same ref is passed to DB::createTable)

  # error checking
  &IT::DB::Common::dieOnFieldDefinitionErrors($fieldDefs);
  if (exists $SCHEMA_TABLES->{$tableName}) { die "table '$tableName' can't be created because it already exists in schema!"; }

  $SCHEMA_TABLES->{$tableName} = {
    'connectionName' => $connectionName,
    'name'           => $tableName,
    'autoSequencing' => 1,
    'fields'         => $fieldDefs,
  };
  &_updateCachedLookupTables();

  return $SCHEMA_TABLES->{$tableName}
}


# -----------------------------------------------------------------------------
# Function    : dropTable
# Description : remove a table from the schema.  Note, this just removes the
#               table from the program schema, it doesn't modify the table in
#               the datastore.
# Usage       : &IT::DB::Schema::dropTable($tableName);
# -----------------------------------------------------------------------------

sub dropTable {
  &IT::DB::_die_unless_init_called();
  my $tableName = shift || &IT::Error::dieAsCaller("No table name specified!");

  if (!exists $SCHEMA_TABLES->{$tableName}) {
    die "table '$tableName' can't be dropped because it doesn't exist in schema!";
  }

  delete $SCHEMA_TABLES->{$tableName};

  return 1;
}


# -----------------------------------------------------------------------------
# Function    : existsTable
# Description : check to see if a given table exists in schema
# Usage       : if (&IT::DB::Schema::existsTable($tableName)) {
# -----------------------------------------------------------------------------

sub existsTable {
  &IT::DB::_die_unless_init_called();
  my $tableName   = shift || &IT::Error::dieAsCaller("No table name specified!");
  my $existsTable = $SCHEMA_TABLES->{$tableName} ? 1 : 0;

  return $existsTable;
}



# -----------------------------------------------------------------------------
# Function    : listTables
# Description : return a sorted list of tables in the schema
# Usage       : my @tables = &IT::DB::Schema::listTables();
# -----------------------------------------------------------------------------

sub listTables {
  &IT::DB::_die_unless_init_called();
  my @schemaTables = sort keys %{$SCHEMA_TABLES};
  return @schemaTables;
}

# -----------------------------------------------------------------------------
# Function    : listTablesOnConnection
# Description : return a sorted list of tables that have the specified connection name
# Usage       : my @tablesOnConnection = &IT::DB::Schema::listTablesOnConnection();
# -----------------------------------------------------------------------------

sub listTablesOnConnection {
  &IT::DB::_die_unless_init_called();
  my $connectionName = shift;

  # error checking
  unless (&IT::DB::Schema::existsConnection($connectionName)) { die "Connection name '$connectionName' doesn't exist!"; }

  # get tables on connection
  my @tablesOnConnection;
  foreach my $tableName (&IT::DB::Schema::listTables()) {
    my $tableConnectionName = &getTableAttributes($tableName, 'connectionName');
    if ($tableConnectionName eq $connectionName) {
      push @tablesOnConnection, $tableName;
    }
  }

  return @tablesOnConnection;
}

# -----------------------------------------------------------------------------
# Function    : renameTable
# Description : Rename a table in the schema.  Note you'll need to call
#               &IT::DB::renameTable() to rename the datafile/sql table
# Usage       : &IT::DB::Schema::renameTable($oldTableName, $newTableName);
# -----------------------------------------------------------------------------

sub renameTable {
  &IT::DB::_die_unless_init_called();
  my $oldTableName = shift || &IT::Error::dieAsCaller("No old table name specified!");
  my $newTableName = shift || &IT::Error::dieAsCaller("No new table name specified!");

  if (!exists $SCHEMA_TABLES->{$oldTableName}) {
    die "table '$oldTableName' can't be renamed because it doesn't exist in schema!";
  }
  if (exists $SCHEMA_TABLES->{$newTableName}) {
    die "table '$oldTableName' can't be renamed because the new table name '$newTableName' already exists!";
  }

  $SCHEMA_TABLES->{$newTableName} = $SCHEMA_TABLES->{$oldTableName};    # copy old table reference for new table key
  $SCHEMA_TABLES->{$newTableName}{'name'} = $newTableName;              # update table name key

  delete $SCHEMA_TABLES->{$oldTableName};                               # remove old table key

  return 1;
}

# -----------------------------------------------------------------------------
# Function    : copyTable
# Description : Copy a table schema in the schema.  Note you'll need to call &IT::DB::createTable()
#               to create that actual table.
# Usage       : &IT::DB::Schema::copyTable($oldTableName, $newTableName);
# -----------------------------------------------------------------------------

sub copyTable {
  &IT::DB::_die_unless_init_called();
  my $oldTableName = shift || &IT::Error::dieAsCaller("No old table name specified!");
  my $newTableName = shift || &IT::Error::dieAsCaller("No new table name specified!");

  if (!exists $SCHEMA_TABLES->{$oldTableName}) {
    die "table '$oldTableName' can't be renamed because it doesn't exist in schema!";
  }
  if (exists $SCHEMA_TABLES->{$newTableName}) {
    die "table '$oldTableName' can't be renamed because the new table name '$newTableName' already exists!";
  }

  $SCHEMA_TABLES->{$newTableName} = &IT::FreezeThaw::structCopy($SCHEMA_TABLES->{$oldTableName});  # copy old table values
  $SCHEMA_TABLES->{$newTableName}{'name'} = $newTableName;                                         # update table name key

  &_updateCachedLookupTables();

  return 1;
}


# -----------------------------------------------------------------------------
# Function    : getTableAttributes
# Description : return an attribute value for a given table (eg: conn, name, defaultSort)
# Usage       : my $attribute = &IT::DB::Schema::getTableAttributes($tableName, $attributeName);
# Usage       : my($attr1, $attr2) = &IT::DB::Schema::getTableAttributes($tableName, 'name', 'fields');
#
# Usage       : my($schemaFieldDefs) = &IT::DB::Schema::getTableAttributes($tableName, 'fields');
#               my @dbFieldDefs      = grep { $_->{'type'} ne 'none' } @$schemaFieldDefs;
# -----------------------------------------------------------------------------

sub getTableAttributes {
  &IT::DB::_die_unless_init_called();
  my $tableName = shift || &IT::Error::dieAsCaller("no table name specified!");
  my @names     = @_;

  # error checking
  if (!@names || !defined $names[0]) {&IT::Error::dieAsCaller("No table attribute name(s) specified!"); }

  # get list of values
  my $tableRef = $SCHEMA_TABLES->{$tableName} || &IT::Error::dieAsCaller("table '$tableName' does not exist in schema!");
  my @values;
  foreach my $name (@names) {
    unless ($VALID_TABLE_ATTRIBUTES{$name}) { die "getTableAttributes: '$name' isn't a valid table attribute!"; }
    push @values, $tableRef->{$name}
  }

  # return values
  if (wantarray) { return @values; }
  else           { return $values[0]; }

}

# -----------------------------------------------------------------------------
# Function    : setTableAttribute
# Description : set an attribute value for a given connection (eg: name, fields, etc)
# Usage       : &IT::DB::Schema::setConnectionAttribute($tableName, $attributeName, $attributeValue);
# -----------------------------------------------------------------------------

sub setTableAttribute {
  &IT::DB::_die_unless_init_called();
  my $tableName = shift || &IT::Error::dieAsCaller("no table name specified!");
  my $name      = shift || &IT::Error::dieAsCaller("no attribute name specified!");
  my $value     = shift;

  # error checking
  unless ($VALID_TABLE_ATTRIBUTES{$name}) { die "setTableAttribute: '$name' isn't a valid table attribute!"; }
  if ($name eq 'name')                    { die "setTableAttribute: can't change 'name' with setTableAttribute, call renameTable instead!"; }

  # set attribute
  my $tableRef = $SCHEMA_TABLES->{$tableName} || &IT::Error::dieAsCaller("table '$tableName' does not exist in schema!");
  $tableRef->{$name} = $value;

  return 1;
}


# ----------------------------------------------------------------------------
# Function    : createField
# Description : create a new field in the schema
# Usage       : &IT::DB::Schema::createField($tableName, $fieldName, $fieldType);
# ----------------------------------------------------------------------------

sub createField {
  &IT::DB::_die_unless_init_called();
  my $tableName = shift || die "No tableName specified!";
  my $fieldName = shift || die "No fieldname specified!";
  my $fieldType = shift || die "No new field type specified!";

  # error checking
  &IT::FileLock::lockRequired('exclusive');
  &IT::DB::Common::dieOnNonExistantTable($tableName);
  &IT::DB::Common::dieOnInvalidFieldName($fieldName);
  if (&IT::DB::Schema::existsField($tableName, $fieldName))    { die "fieldname '$fieldName' already exists in table!"; }
  if (!$VALID_FIELD_TYPES{$fieldType} && $fieldType ne 'none') { die "Invalid field type '$fieldType'"; }

  ### update field defs
  my $fieldDefs   = $SCHEMA_TABLES->{$tableName}{'fields'};
  my $newFieldDef = { 'name' => $fieldName, 'type' => $fieldType, };
  push @$fieldDefs, $newFieldDef;
  &_updateCachedLookupTables();

}


# ----------------------------------------------------------------------------
# Function    : dropField
# Description : drop a field from the schema.  Save schema to save changes
# Usage       : &IT::DB::Schema::dropField($tableName, $fieldName);
# ----------------------------------------------------------------------------

sub dropField {
  &IT::DB::_die_unless_init_called();
  my $tableName = shift || die "No tableName specified!";
  my $fieldName = shift || die "No fieldname specified!";

  # error checking
  &IT::FileLock::lockRequired('exclusive');
  &IT::DB::Common::dieOnNonExistantTable($tableName);
  &IT::DB::Common::dieOnInvalidFieldName($fieldName);
  if (!&IT::DB::Schema::existsField($tableName, $fieldName)) { die "fieldname '$fieldName' doesn't exist in table!"; }

  ### remove field from field defs
  my $oldFieldDefs = $SCHEMA_TABLES->{$tableName}{'fields'};
  my $newFieldDefs = [];
  my $removedField = 0;
  foreach my $fieldDef (@$oldFieldDefs) {
    if ($fieldDef->{'name'} eq $fieldName) {
      $removedField = 1;
      next;
    }
    push @$newFieldDefs, $fieldDef;
  }
  if (!$removedField) { die "Couldn't find drop field '$fieldName'!"; }

  ### update field defs
  $SCHEMA_TABLES->{$tableName}{'fields'} = $newFieldDefs;
  &_updateCachedLookupTables();
}


# ----------------------------------------------------------------------------
# Function    : changeFieldNameAndType
# Description : change a field name and type in the schema, dies on errors
# Usage       : &IT::DB::Schema::changeFieldNameAndType($tableName, $oldFieldName, $newFieldName, $newFieldType);
# ----------------------------------------------------------------------------

sub changeFieldNameAndType {
  &IT::DB::_die_unless_init_called();
  my $tableName    = shift || die "No tableName specified!";
  my $oldFieldName = shift || die "No old fieldname specified!";
  my $newFieldName = shift || die "No new fieldname specified!";
  my $newFieldType = shift || die "No new field type specified!";

  # error checking
  &IT::FileLock::lockRequired('exclusive');
  &IT::DB::Common::dieOnNonExistantTable($tableName);
  &IT::DB::Common::dieOnInvalidFieldName($newFieldName);
  if (!&IT::DB::Schema::existsField($tableName, $oldFieldName)) { die "original fieldname '$oldFieldName' doesn't exist in table!"; }
  if ($oldFieldName ne $newFieldName &&
      &IT::DB::Schema::existsField($tableName, $newFieldName))       { die "new fieldname '$newFieldName' already exists in table!"; }
  if (!$VALID_FIELD_TYPES{$newFieldType} && $newFieldType ne 'none') { die "Invalid field type '$newFieldType'"; }

  ### update field defs
  &IT::DB::Schema::setFieldAttribute($tableName, $oldFieldName, 'type', $newFieldType);
  &IT::DB::Schema::setFieldAttribute($tableName, $oldFieldName, 'name', $newFieldName);

  #
  &_updateCachedLookupTables();

}

# -----------------------------------------------------------------------------
# Function    : existsField
# Description : Check if a field exists in the schema and return 0 or 1.
# Usage       : my $exists = &IT::DB::Schema::existsField($tableName, $fieldName);
# -----------------------------------------------------------------------------

sub existsField {
  &IT::DB::_die_unless_init_called();
  my $tableName = shift || &IT::Error::dieAsCaller("No table name specified!");
  my $fieldName = shift || &IT::Error::dieAsCaller("No field name specified!");

  my $fieldDefs  = $SCHEMA_TABLES->{$tableName}{'fields'};
  my %fieldNames = map { $_->{'name'} => 1 } @$fieldDefs;

  if ($fieldNames{$fieldName})  { return 1; }
  else                          { return 0; }

}

# -----------------------------------------------------------------------------
# Function    : getFieldAttributes
# Description : return a field attribute for a given field name (eg: name, type, index, etc)
# Usage       : my $attribute = &IT::DB::Schema::getFieldAttributes($tableName, $fieldName, $attributeName);
# Usage       : my($attr1,$attr2) = &IT::DB::Schema::getFieldAttributes($tableName, $fieldName, 'type', 'index');
#
# NOTE        : For must faster (5x or more) lookups (and no error checking) access this global directly:
#               $IT::DB::Schema::FIELD_ATTRIBUTES->{$tableName}{$fieldName}{$attributeName};
# -----------------------------------------------------------------------------

sub getFieldAttributes {
  &IT::DB::_die_unless_init_called();
  if (@_ < 3) {&IT::Error::dieAsCaller("No field attribute name(s) specified!"); }
  my $tableName = shift || &IT::Error::dieAsCaller("no table name specified!");
  my $fieldName = shift || &IT::Error::dieAsCaller("no field name specified!");
  my @attrNames = @_;

  # lookup attribute value
  my @attrValues;
  my $fieldDef = $CACHED_LOOKUP_TABLES->{'fieldDefsByName'}{$tableName}{$fieldName} || die "couldn't find field '$fieldName' in table '$tableName'!";

  foreach my $attrName (@attrNames) {
    my $attrValue = $fieldDef->{$attrName};
    if (!$attrValue && !$VALID_FIELD_ATTRIBUTES{$attrNames[0]}) { die "getFieldAttributes: '$attrNames[0]' isn't a valid field attribute!"; }
    push @attrValues, $attrValue;
  }

  # return values
  if (wantarray) { return @attrValues; }
  else           { return $attrValues[0]; }
}

# -----------------------------------------------------------------------------
# Function    : setFieldAttribute
# Description : set an attribute value for the field (eg: name, type, index)
# Usage       : &IT::DB::Schema::setFieldAttribute($tableName, $fieldName, $attributeName, $attributeValue);
# -----------------------------------------------------------------------------

sub setFieldAttribute {
  &IT::DB::_die_unless_init_called();
  my $tableName = shift || &IT::Error::dieAsCaller("no table name specified!");
  my $fieldName = shift || &IT::Error::dieAsCaller("no field name specified!");
  my $name      = shift || &IT::Error::dieAsCaller("no field attribute name specified!");
  my $value     = shift;

  # error checking
  unless ($VALID_FIELD_ATTRIBUTES{$name}) { die "setFieldAttribute: '$name' isn't a valid field attribute!"; }

  # set attribute
  my $fieldDef = $CACHED_LOOKUP_TABLES->{'fieldDefsByName'}{$tableName}{$fieldName} || die "couldn't find field '$fieldName' in table '$tableName'!";
  $fieldDef->{$name} = $value;

  return 1;
}

# -----------------------------------------------------------------------------
# Function    : getFieldIndex
# Description : return the index of the field from the field list in the table schema
# Usage       : my $index = &IT::DB::Schema::getFieldIndex($tableName, $fieldName);
# -----------------------------------------------------------------------------

sub getFieldIndex {
  &IT::DB::_die_unless_init_called();
  my $tableName = shift || &IT::Error::dieAsCaller("No table name specified!");
  my $fieldName = shift || &IT::Error::dieAsCaller("No field name specified!");

  my $fieldDefs   = $SCHEMA_TABLES->{$tableName}{'fields'};
  my %nameToIndex;
  my $index = 0;
  foreach my $fieldDef (@$fieldDefs) {
    if ($fieldDef->{'type'} eq 'none') { next; } # skip fields with type none, they exist in schema only, not database
    $nameToIndex{ $fieldDef->{'name'} } = $index;
    $index++;
  }

  my $fieldIndex = $nameToIndex{$fieldName};

  if ($fieldIndex eq '') {
    my $validFieldsAsCSV = join ", ", keys %nameToIndex;
    die "couldn't find index for field '$fieldName' in table '$tableName'!\nValid fieldnames: $validFieldsAsCSV";

  }

  return $fieldIndex;
}


# -----------------------------------------------------------------------------
# Function    : getFieldNames
# Description : return a list of field names in the table, in order
# Usage       : my @fields = &IT::DB::Schema::getFieldNames($tableName, 'INCLUDE_SCHEMA_ONLY_FIELDS');
# -----------------------------------------------------------------------------

sub getFieldNames {
  &IT::DB::_die_unless_init_called();
  my $tableName            = shift || &IT::Error::dieAsCaller("No table name specified!");
  my $includeSchemaOnlyFields = shift;

  # error checking
  if ($includeSchemaOnlyFields && $includeSchemaOnlyFields ne 'INCLUDE_SCHEMA_ONLY_FIELDS') {
    die "\$includeSchemaOnlyFields must be blank or 'INCLUDE_SCHEMA_ONLY_FIELDS'!\n";
  }

  # get fieldnames
  my @fieldNames;
  my $fieldDefs  = $SCHEMA_TABLES->{$tableName}{'fields'};
  my $index = 0;
  foreach my $fieldDef (@$fieldDefs) {
    if (!$includeSchemaOnlyFields && $fieldDef->{'type'} eq 'none') { next; } # skip fields with type none, they exist in schema only, not database
    push @fieldNames, $fieldDef->{'name'};
    $index++;
  }

  return @fieldNames;
}

# -----------------------------------------------------------------------------
# Function    : generateNewFieldname
# Description : Create a new unused fieldname
# Usage       : my $fieldname = &IT::DB::Schema::generateNewFieldname($tableName, $fieldnamePrefix, $fieldnameSuffix);
# -----------------------------------------------------------------------------

sub generateNewFieldname {
  &IT::DB::_die_unless_init_called();
  my $tableName       = shift || &IT::Error::dieAsCaller("No table name specified!");
  my $fieldnamePrefix = shift || &IT::Error::dieAsCaller("no fieldname prefix specified!");
  my $fieldnameSuffix = shift;

  # load existing fieldnames
  my @fields = &IT::DB::Schema::getFieldNames($tableName, 'INCLUDE_SCHEMA_ONLY_FIELDS');
  my %fields = map { $_ => 1 } @fields;

  # generate new fieldname
  my $newFieldname;
  my $counter = 0;
  while (!$newFieldname || exists $fields{ $newFieldname }) {
    $newFieldname = $fieldnamePrefix . ++$counter . $fieldnameSuffix;
  }

  return $newFieldname;
}

# ----------------------------------------------------------------------------
# end of file
1;

