<?php

/**
 * IsisImporter: provides ISIS import methods for importing data into
 * a Symfony project.
 */
class sfIsisImporter extends IsisConnector
{
  /**
   * Log dispatcher.
   *
   * @param string $message Log message
   * @param string $level   Log level
   */
  public function log($message, $level = 'info')
  {
    $this->logger->log($message, $level);
  }

  /**
   * Constructor.
   */ 
  public function __construct($config = null) {
    parent::__construct($config);
    $this->stats = sfIsisImporterStats::getInstance();

    // Get a logger instance.
    $this->logger = sfIsisImporterLog::getInstance();
  }

  /**
   * Guess a method name from a type.
   *
   * @param  string $type Mapping type
   * @return string       Method name
   */
  static function methodName($type)
  {
    return 'import'. ucfirst($type);
  }

  /**
   * Get the model foreign table id.
   *
   * @param  object $model Model
   * @return string        Model table id
   */
  static function getModelId($model) {
    return sfInflector::underscore(get_class($model)) .'_id';
  }

  /**
   * Get the relation foreign table id.
   *
   * @param  string $model Relation name
   * @return string        Relation table id
   */
  static function getRelationId($relation) {
    return sfInflector::underscore($relation) .'_id';
  }

  /**
   * Get the model and relation tablename.
   *
   * @param  object $model Model
   * @param  string $model Relation name
   * @return string        Relation table name
   */
  static function getModelRelation($model, $relation)
  {
    return sfInflector::camelize(self::getModelName($model)) . sfInflector::camelize($relation);
  }

  /**
   * Get the entity name from a subfield.
   *
   * @param  string $subfield Subfield name
   * @return string           Genre name
   */
  static function entityName($subfield)
  {
    return ucfirst($subfield);
  }  

  /**
   * Get the model name.
   *
   * @param  object $model Model
   * @return string        Model name
   */
  static function getModelName($model)
  {
    return get_class($model);
  }

  /**
   * Add a single entry from the database.
   *
   * @param string $base_model Model to use
   * @param int    $entry      Entry number
   */
  public function addEntry($base_model, $entry)
  {
    // Get data and setup the model.
    $this->read($entry);
    $model = $this->newModel($base_model, $entry);

    if ($model)
    {
      $this->log("Importing $base_model $entry...");

      // Dispatch to custom import procedures.
      foreach (new IsisMethodIterator($this) as $method => $field)
      {
        $this->{$method}($model, $field);
      }      

      $model->save();
    }
    else {
      $this->log("Skipping existing entry $entry for $base_model.");
    }
  }

  /**
   * Create a new model just if doesn't exist for a given entry. For that
   * to work the entry must provide and id.
   *
   * @param  string $base_model Model to use
   * @param  int    $entry      Entry number
   * @return mixed              New model or false
   */
  public function newModel($base_model, $entry)
  {
    $model = new $base_model();
    $id    = $this->getBaseModelId($model);

    if ($id)
    {
      if (!call_user_func(array($base_model, 'getById'), $id))
      {
        $this->setBaseModelId($model);
        return $model;
      }
      else
      {
        return false;
      }
    }

    return $model;
  }

  /**
   * Set the primary key for the model by getting it or just saving it.
   *
   * @param object $model Base model
   */
  public function setBaseModelId(&$model)
  {
    $model->id = $this->getBaseModelId($model);
    $model->save();
  }

  /**
   * Get the primary key for the base model.
   *
   * @param  object $model Base model
   * @return int           Base model id
   */
  public function getBaseModelId(&$model)
  {
    $method = get . $this->getModelName($model) .'PrimaryKey';
    if (method_exists($this, $method))
    {
      return $this->{$method}($model);
    }
  }

  /**
   * Import a single field into a model.
   *
   * @param object $model    Model
   * @param array  $field    Field data from ISIS database schema
   * @param int    $row      Row number
   */
  public function addField(&$model, $field, $row = 0) {
    $value = $this->filterBrackets($this->getMainItem($field, $row));

    if ($value != null)
    {
      $map   = $this->getMap($field);
      $model->{$map}($value);
    }
  }

  /**
   * Import a single subfield into a model.
   *
   * @param object $model    Model
   * @param array  $field    Field data from ISIS database schema
   * @param string $subfield Subfield name
   * @param int    $row      Row number
   */
  public function addSubfield(&$model, $field, $subfield, $row = 0) {
    $value = $this->filterBrackets($this->getSubfield($field, $subfield, $row));

    if ($value != null)
    {
      $map   = $this->getMap($field, $subfield);
      $model->{$map}($value);
    }
  }

  /**
   * Import single values into the model.
   *
   * Currently undefined mappings for a field/subfield
   * are not saved as we would need to make sure a corresponding
   * field exists in the model. This prevents the map
   * configurations like $map = array('type' => 'value'); to work.
   *
   * As we are importing single values, here we don't care with
   * row numbers as we assume that just the first row should be
   * imported.
   *
   * @param object $model Model object
   * @param array  $field Field data
   */
  public function importValues(&$model, array $field)
  {
    if ($this->fieldHasMap($field))
    {
      $this->addField($model, $field);
    }

    foreach ($this->getSubfieldList($field) as $subfield)
    {
      if ($this->subfieldHasMap($field, $subfield))
      {
        $this->addSubfield($model, $field, $subfield);
      }
    }
  }

  /**
   * Add a new entity into the database if needed, returning
   * the corresponding object.
   *
   * @param  string $name Genre name
   * @return Genre        Genre data
   */
  public function addEntity($entity, $name)
  {
    $name = $this->entityName($name);
    $data = Doctrine_Core::getTable($entity)->findOneByName($name);

    if (!$data)
    {
      $this->log("Adding new $entity $name.");
      $data       = new $entity();
      $data->name = $name;
      $data->save();
    }

    return $data;
  }

  /**
   * Import one to one data.
   *
   * @param object $model    Model
   * @param array  $field    Field data from ISIS database schema
   * @param string $relation Relation name
   */
  public function addOneToOne(&$model, array $field, $relation)
  {
    foreach (new IsisRowIterator($this, $field) as $row)
    {
      $data = new $relation();

      foreach ($this->getSubfieldList($field) as $subfield)
      {
        $this->addSubfield($data, $field, $subfield, $row);
      }

      $data->save();
      $key         = sfInflector::underscore($relation) .'_id';
      $model->$key = $relation->id;
    }
  }

  /**
   * Import many to many data.
   *
   * @param object $model    Model
   * @param array  $values   Values to be added
   * @param string $relation Relation name
   */
  public function addManyToMany(&$model, array $values, $relation) {
    $method = 'add'. $relation;

    foreach ($values as $value)
    {
      // Populate related data.
      if (is_callable(array($this, $method)))
      {
        $data = $this->{$method}($value);
      }
      else
      {
        $data = $this->addEntity($relation, $value);
      }

      // Get model and relation names and id fields.
      $model_id                   = $this->getModelId($model);
      $relation_id                = $this->getRelationId($relation);
      $model_relation             = $this->getModelRelation($model, $relation);

      // Make the relation.
      $model_data                 = new $model_relation();
      $model_data->{$model_id}    = $model->id;
      $model_data->{$relation_id} = $data->id;
      $model_data->save();
    }
  }

  /**
   * Import one to one data.
   *
   * @param  object $model    Model
   * @param  string $relation Relation name
   * @return object           Relation model object
   */  
  public function addOneToMany(&$model, $relation)
  {
    $model_id          = $this->getModelId($model);
    $data              = new $relation();
    $data->{$model_id} = $model->id;
    $data->save();
    return $data;
  }

  /**
   * Add simple entities data into the model.
   *
   * @param object $model Model
   * @param array  $field Field data from ISIS database schema
   */  
  public function addOneToManyEntities(&$model, array $field, $entity, $key = 'name')
  {
    foreach (new IsisMainItemIterator($this, $field) as $row => $value)
    {
      $this->log("Entity: $entity; Value: $value", 'debug');
      $data         = $this->addOneToMany($model, $entity);
      $data->{$key} = $value;
      $data->save();
    }    
  }

  /**
   * Add field values in a many-to-many relation.
   *
   * @param object $model    Model
   * @param array  $field    Field data from ISIS database schema
   * @param string $relation Relation name
   */
  public function addManyToManyField(&$model, array $field, $relation)
  {
    foreach (new IsisMainItemIterator($this, $field) as $value)
    {
      $this->addManyToMany($model, $this->explodeBrackets($value), $relation);
    }    
  }

  /**
   * Add an element into the database if needed, returning
   * the resulting object.
   *
   * @param  string $entity Entity name
   * @param  string $value  Entity value
   * @return object         Entity data
   */
  public function newOrExisting($entity, $value)
  {
    // Check for a null value.
    if ($value == null)
    {
      $this->log("Null element value for $entity.", 'debug');
      return;
    }

    // Get name.
    $name = $this->parseName($value);

    // Get existing element.
    $element = call_user_func(array($entity, 'getByName'), $name);

    // Create new element if needed.
    if (!$element)
    {
      $this->log("Adding new $entity $value.");
      $element = call_user_func(array($entity, 'addByName'), $name);
    }

    return $element;
  }
}