<?php
/**
 * @file opencalais.module
 */

//Include the utils file which provides all the field utility functions required to create and remove fields, and some purely utility functions
include_once('opencalais.utils.inc');

include_once('opencalais.export.inc');

/**
 *  Implementation of hook_menu().
 */
function opencalais_menu() {
  $items = array();

  $items['admin/config/content/opencalais/tagging'] = array(
    'title' => 'Tagging',
    'description' => 'Configure Entity Tagging',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('opencalais_admin_general_settings'),
    'access arguments' => array('administer opencalais'),
    'file' => 'opencalais.admin.inc',
    'type' => MENU_LOCAL_TASK,
  );

  $items['admin/structure/types/manage/%/opencalais_fields'] = array(
    'title' => 'OpenCalais Fields',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('opencalais_add_fields_form', 1),
    'access arguments' => array('administer opencalais'),
    'file' => 'opencalais.admin.inc',
    'type' => MENU_LOCAL_TASK,
  );

  $items['admin/config/content/opencalais/bulk'] = array(
    'title' => 'Bulk Operations',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('opencalais_bulk_operations_form'),
    'access arguments' => array('administer opencalais'),
    'file' => 'opencalais.bulk.inc',
    'type' => MENU_LOCAL_TASK
  );

  $items['admin/config/content/opencalais/bulk/%'] = array(
    'title' => t('Process Content Type'),
    'page callback' => 'opencalais_add_items_to_queue_callback',
    'page arguments' => array(5),
    'access arguments' => array('administer opencalais'),
    'file' => 'opencalais.bulk.inc',
    'type' => MENU_CALLBACK
  );
  
 $items['admin/config/content/opencalais/filters'] = array(
   'title' => 'OpenCalais Filters',
   'page callback' => 'drupal_get_form',
   'page arguments' => array('opencalais_filters_config_form'),
   'access arguments' => array('administer opencalais'),
   'file' => 'opencalais.filters.inc',
   'type' => MENU_LOCAL_TASK
 );
 
 $items['admin/config/content/opencalais/filters/%'] = array(
   'title' => t('Configure Filter'),
   'page callback' => 'opencalais_filters_configure_filter',
   'page arguments' => array(5),
   'access arguments' => array('administer opencalais'),
   'file' => 'opencalais.filters.inc',
   'type' => MENU_NORMAL_ITEM
 );
 
  return $items;
}

/**
 * Implements hook_theme().
 */
function opencalais_theme($existing, $type, $theme, $path) {
  $path = drupal_get_path('module', 'opencalais');
  return array(
    'opencalais_suggestions' => array(
      'variables' => array('type' => NULL, 'field_name' => NULL, 'suggestions' => NULL, 'language' => LANGUAGE_NONE),
      'path' => "$path/theme",
      'template' => 'opencalais_suggestions',
    ),
    'opencalais_preset_form' => array(
      'render element' => 'form',
      'path' => "$path/theme",
      'file' => 'theme.inc',
    ),
    'opencalais_add_fields_entities' => array(
      'render element' => 'info',
      'path' => "$path/theme",
      'file' => 'theme.inc',
    ),
    'opencalais_node_fields' => array(
      'variables' => array('html' => NULL),
      'path' => "$path/theme",
      'template' => 'opencalais_node_fields',
    ),
  );
}

/**
 * Implements hook_form_BASE_FORM_ID_alter().
 * Add ajax callbacks to the opencalais fields
 */
function opencalais_form_node_form_alter(&$form, &$form_state, $form_id) {  
  $node = $form['#node'];
  $fields = opencalais_get_opencalais_tag_fields($form, 'node', $node->type);
  $extra = array();

  foreach ($fields as $opencalais_type => $field_name) {
    // Load suggestions
    $suggestions = opencalais_get_suggestions($node, $opencalais_type);
    
    $auto = variable_get('opencalais_autotagging', array());
    $auto = isset($auto[$node->type]) ? $auto[$node->type] : 0;
   
    $vars = array(
      'type' => $opencalais_type, 
      'field_name' => $field_name, 
      'suggestions' => $suggestions, 
      'language' => LANGUAGE_NONE,
    );

    $themed_suggestions = theme('opencalais_suggestions', $vars); 
    $form[$field_name]['#suffix'] = $themed_suggestions;
    if ($auto) {
      $form[$field_name][$form[$field_name]['#language']]['#default_value'] = _opencalais_make_field_values($suggestions);
    }
    
    $extra[$field_name] = $suggestions; 
  }
  $form_state['opencalais_extras'] = $extra;
  
  if ($fields) {
    $path = drupal_get_path('module', 'opencalais');
    $form['actions']['suggest_tags'] = array(
      '#type' => 'submit', 
      '#value' => t('Suggest tags'),
      '#prefix' => '<div class="opencalais_button_holder">',
      '#suffix' => '</div>',
      '#attributes' => array('class' => array('opencalais_submit')),
      '#weight' => -20,
      '#submit' => array('opencalais_suggest_tags_submit'),     
      '#ajax' => array(
        'callback' => 'opencalais_suggest_tags_callback',  
        'effect' => 'fade',
      ),
      '#attached' => array(
        'js' => array($path . '/theme/opencalais.node.js'),
        'css' => array($path . '/theme/opencalais.node.css'),
      )
    );
            
    // Should we collect them in vertical tabs?
    if (variable_get('opencalais_tags_in_verticaltab', TRUE)) {
      $form['opencalais']  = array(
        '#type' => 'fieldset', 
        '#title' => t('OpenCalais tags'), 
        '#collapsible' => TRUE, 
        '#collapsed' => TRUE, 
        '#group' => 'additional_settings', 
        '#weight' => -2, 
      );
      
      $form['opencalais']['suggest_tags'] = $form['actions']['suggest_tags'];
      unset($form['actions']['suggest_tags']);
          
      foreach ($fields as $field_name) {
        $form['opencalais'][$field_name] = $form[$field_name];
        hide($form[$field_name]);
      }
    }
    
    array_unshift($form['#submit'], 'opencalais_node_form_submit');
  }
}

/**
 *  Handle the submission of the node form
 *
 *  If automatic tagging is set for the content type then get the term suggestions and place them into values
 *  arrays for all the opencalais fields
 *
 *  TODO: See if we can also add in the addition of the disambiguation information here
 */
function opencalais_node_form_submit($form, &$form_state) {
  if (isset($form_state['opencalais_building']) && $form_state['opencalais_building']) { 
    return; 
  }

  $content_type = $form_state['values']['type'];

  //Add extra meta data to the taxonomy term items
  $lang_code = $form['language']['#value'];
  //find the the extra fields with extra values
  $extras = $form_state['opencalais_extras'];

  foreach ($extras as $field=>$value) {
    if (isset($form_state['values'][$field])) {
      $lang = isset($form_state['values'][$field][$lang_code]) ? $lang_code : LANGUAGE_NONE;
      $field_values = $form_state['values'][$field][$lang];
      foreach ($field_values as $i => $v) {
        if (isset($extras[$field][$v['name']])) {
          $eV = $extras[$field][$v['name']]['extra'];
          foreach ($eV as $n=>$extra_val) {
            $form_state['values'][$field][$lang][$i][$n][$lang] = array();
            $form_state['values'][$field][$lang][$i][$n][$lang][] = array('value' => $extra_val);
          }//end foreach
        }
      }//end foreach
    }
  } //end foreach
}

/** 
 *  Implements hook_node_presave
 *
 *  Used to add fields if auto tagging is turned on and the field hasn't already been tagged
 *  This will be used when nodes are created in some way other than through the forms system
 */
function opencalais_node_presave($node){
  $content_type = $node->type;
  $auto = variable_get('opencalais_autotagging', array());
  $auto = isset($auto[$content_type]) ? $auto[$content_type] : 0;

  if ($auto && !_opencalais_already_tagged($node)) {
    $fields = opencalais_get_fields_for_content_type($node->type);

    $suggestions = array();
    foreach ($fields as $opencalais_type => $field) {
      $name = $field['field'];
      $real_field = field_read_field($name);
                  
      $suggestions = opencalais_get_suggestions($node, $opencalais_type);
      if($suggestions){
        $node->{$name} = array(
          $node->language => array()
        );
      
        foreach ($suggestions as $term=>$meta) {
          $term = _opencalais_create_term($term, $real_field, $node, $meta);
          $node->{$name}[$node->language][] = $term;
        }
      
        $instance = field_read_instance('node', $name, $node->type);
        //run presave as it has already been run on this entity and won't be called again before node_save
        taxonomy_field_presave('node', $node, $real_field, $instance, $node->language, $node->{$name}[$node->language]);
      }
    }
  }
}

/**
 * AJAX Callback to get OpenCalais tag suggestions for an Entity.
 */
function opencalais_get_opencalais_tag_fields($form, $entity, $bundle) {
  $fields = array();
  $entities = opencalais_get_all_entities();
  foreach ($entities as $key=>$item) {
    $entities[$key] = _opencalais_make_machine_name($item);
  }
  $instances = field_info_instances($entity, $bundle);
  foreach ($instances as $field_name => $instance) {
    
    if ( isset($instance['settings']['opencalais'])) {
      $field = field_info_field($field_name);
      $opencalais_type = $instance['settings']['opencalais'];    
      if (in_array($opencalais_type, $entities)) {
        $fields[$opencalais_type] = $field_name;
      }
    }
  }
  
  return $fields;
}


/**
 * AJAX Callback to get OpenCalais tag suggestions for an Entity.
 */
function opencalais_suggest_tags_callback($form, &$form_state, $norebuild=FALSE) {
  $form_state['opencalais_building'] = true;
  $node = node_form_submit_build_node($form, $form_state);  
  $form_state['opencalais_building'] = false;  
  $fields = opencalais_get_opencalais_tag_fields($form, 'node', $node->type);
  
  // Load suggestions  
  $commands = array();
  $extra = array();
  $form_state['opencalais_suggestions'] = array();
  
  foreach ($fields as $opencalais_type => $field_name) {
    $suggestions = opencalais_get_suggestions($node, $opencalais_type);
    
    $form_state['opencalais_suggestions'][$field_name] = $suggestions;
    $vars = array(
      'type' => $opencalais_type, 
      'field_name' => $field_name, 
      'suggestions' => $suggestions, 
      'language' => LANGUAGE_NONE,
    );
    $themed_suggestions = theme('opencalais_suggestions', $vars); 
    $commands[] = ajax_command_replace("#{$field_name}_suggestions", $themed_suggestions);        

    //add extra fields to the session for storage since apparently we can't write to form state
    $extra[$field_name] = $suggestions; 
  }
  
  $form_state['opencalais_extras'] = $extra;
  if (!$norebuild) {
    $form_state['rebuild'] = TRUE;

    /**
     *  Because the form state isn't resaved in ajax_form_callback anything we put in the form_state gets destroyed
     *  In order to keep our form_state stuff (the meta data) we need to do this.
     */
    drupal_process_form($form['#form_id'], $form, $form_state);
  }
  return array('#type' => 'ajax', '#commands' => $commands);
}

/**
 * Gracefully handle JS degredation by providing a multi-step form implementation
 */
function opencalais_suggest_tags_submit($form, &$form_state) {
  $node = node_form_submit_build_node($form, $form_state);
  $suggestions = opencalais_get_suggestions($node);
}

/**
 *  Retrieve suggestions from OpenCalais service 
 */
function opencalais_get_suggestions(&$node, $opencalais_type = NULL) {
  $tag_cache = &drupal_static(__FUNCTION__);
  
  if ($tag_cache && isset($node->ocid) && array_key_exists($node->ocid, $tag_cache)) {
    $suggestions = $tag_cache[$node->ocid];    
  }
  else {
    if (!$node->title) {
      //we need some way to break out if its a node being prepped to show on the node form (with no content)
      return;
    }
     
    // Needed to support caching of unsaved nodes
    if (empty($node->ocid)) {
      $node->ocid = !empty($node->nid) ? $node->nid : uniqid();
    }
    
    if (!empty($node->nid)) {
      $elements = node_view($node);
      //$body = strip_tags(drupal_render($elements));
      $body = drupal_render($elements);
      $date = format_date($node->created, 'custom', 'r');
    } 
    else {
      $body = $node->body[$node->language][0]['value'];
      $date = format_date(REQUEST_TIME, 'custom', 'r');
    }
    
    $html_array['title'] = $node->title;

    // adding all text fields and text area in the node to be 
    // evalulated in OC
    $settings = variable_get('opencalais_settings', array());
    $html_array['fields'] = array();
    if (array_key_exists('node_fields', $settings)) {
      if (array_key_exists($node->type, $settings['node_fields'])) {
        if ((bool) $settings['node_fields'][$node->type]) {
          $node_fields = field_info_instances('node', $node->type);
          foreach ($node_fields as $field_name => $instance) {
            // add all textfields and textareas
            if ($instance['widget']['module'] === 'text') {
              if (empty($node->{$field_name})) {
                continue;
              }
              // get all of that field into the html render array
              foreach ($node->{$field_name}[$node->language] as $item) {
                $html_array['fields'][$field_name][] = $item['value'];
              }
            }
          }
        }
      }
    }
    
    $html = theme('opencalais_node_fields', array('html' => $html_array));
      
    // Allow modification of the content sent to Calais
    drupal_alter("opencalais_body", $html, $node);
        
    $opencalais = opencalais_api_get_service();
    //$tags = $opencalais->analyzeXML($node->title, $body, $date);
    $tags = $opencalais->analyzeHTML($html);
    
    //get a list of all the fields for this content type
    $fields = opencalais_get_fields_for_content_type($node->type);

    $suggestions = array();    
    foreach ($tags as $type => $metadata) {
      $terms = array();  
      $machine_name = _opencalais_make_machine_name($type);
      
      if (isset($fields[$machine_name])) {
        $settings = field_info_instance('node', $fields[$machine_name]['field'], $node->type);
        foreach ($metadata->terms as $guid => $term) {
          //only add it if its relevant
          if(opencalais_check_suggestion($settings, $term)){
            $terms[$term->name] = array( 
              'relevance' => (float)$term->relevance,
              'extra' => $term->extra
            );
          }
        }  
      }
      $suggestions[$machine_name] = $terms;
    }   
  }

  //Allow other modules to alter the suggestions before they are cached.
  $data = array('content_type' => $node->type, 'tags' => $suggestions);
  drupal_alter('opencalais_tags', $data);
  //not sure we need this - but lets be sure
  $suggestions = $data['tags'];
  
  $tag_cache[$node->ocid] = $suggestions;
  
  //Return the suggestions - for the oc type if its set or all of them
  if (isset($opencalais_type)) {
    return isset($suggestions[$opencalais_type]) ? $suggestions[$opencalais_type] : array();
  } else {
    return $suggestions;
  }
}

/**
 *  Check whether the term should apply based on the field instance settings
 */
function opencalais_check_suggestion($settings, $term){
  
  if ($settings && is_array($settings['settings']) && isset($settings['settings']['threshold'])) {
    return $settings['settings']['threshold'] <= $term->relevance;
  }
}

/**
 * Get a list of the entities that OpenCalais API defines:
 *    http://d.opencalais.com/1/type/em/e/.html
 *
 * @return array of OpenCalais entities, use local defaults if they cannot be retrieved remotely
 */
function opencalais_get_all_entities() {
  $entities = &drupal_static(__FUNCTION__);
  if (!empty($entities)) {
    return $entities;
  }
  
  $entities = cache_get('opencalais_entities');
  if ($entities) {
   $entities = $entities->data;   
    return $entities;
  }
  // Try to load the entities automagically from opencalais
  $entities = array();
  $response = drupal_http_request('http://d.opencalais.com/1/type/em/e/.html');
  if (!isset($response->error)) {
    $cleaned = preg_replace('/<(link|META)(.*)>/', '', $response->data);
    $doc = simplexml_load_string($cleaned);
    $spans = $doc->xpath("//span[@rtype='entity']");
    
   
    foreach ($spans as $span) {
      $entities[] = (string)$span['label'];       
    }
  }
  else {
    // Defaults
    $entities = array(
      'Anniversary',
      'City',
      'Company',
      'Continent',
      'Country',
      'Currency',
      'EmailAddress',
      'EntertainmentAwardEvent',
      'Facility',
      'FaxNumber',
      'Holiday',
      'IndustryTerm',
      'MarketIndex',
      'MedicalCondition',
      'MedicalTreatment',
      'Movie',
      'MusicAlbum',
      'MusicGroup',
      'NaturalDisaster',
      'NaturalFeature',
      'OperatingSystem',
      'Organization',
      'Person',
      'PhoneNumber',
      'PoliticalEvent',
      'Position',
      'Product',
      'ProgrammingLanguage',
      'ProvinceOrState',
      'PublishedMedium',
      'RadioProgram',
      'RadioStation',
      'Region',
      'SportsEvent',
      'SportsGame',
      'SportsLeague',
      'Technology',
      'TVShow',
      'TVStation',
      'URL',
      'NaturalDisaster',
      'PoliticalEvent',
    );
  }
    
  // Special Reserved Vocabularies    
  array_push($entities, 'SocialTags', 'CalaisDocumentCategory', 'EventsFacts');
  sort($entities);

  cache_set('opencalais_entities', $entities, 'cache', time() + (60 * 60 * 24));  
  return $entities;
}
 
/**
 *  Implements hook_form_FORM_ID_alter()
 *
 *  Add the threshold setting the field settings form for opencalais fields
 */    
function opencalais_form_field_ui_field_edit_form_alter(&$form, &$form_state, $form_id){
  //only show the threshold editor for nodes created by opencalais (nodes that already have a threshold and have the correct name)
  if (isset($form['#instance']['settings']['threshold']) && stristr($form['#field']['field_name'], 'opencalais_')) {
    $form['instance']['settings']['threshold'] = array(
      '#type' => 'textfield',
      '#title' => t('OpenCalais Threshold Value'),
      '#description' => t('How relevant a term must be to be applied to a node. This is only utilized on OpenCalais Fields.'),
      '#default_value' => $form['#instance']['settings']['threshold'],
    );
    if(!is_array($form['#validate'])){
      $form['validate'] = array();
    }
    $form['#validate'][] = 'opencalais_check_threshold';
  }
}

/**
 *  Simple Validator for the system settings form;
 */
function opencalais_check_threshold($form, $form_state){
  $threshold = $form_state['values']['instance']['settings']['threshold'];
  if (!isset($threshold) || $threshold < 0 || $threshold > 1) {
    form_set_error('instance[settings][threshold]', 'Threshold must be between 0 and 1');
  } 
}


/**
 *  Proprocessor for the system_settings form form to move OpenCalais Terms to the sidebar (for rubik)
 */
function opencalais_preprocess_system_settings_form(&$variables){
  if ($variables['form']['#form_id'] == 'opencalais_add_fields_form') {
    //push the help into the sidebar - this will work on rubik - not sure what else right now.
    $variables['sidebar'][] = $variables['form']['help'];
    unset($variables['form']['help']);
    
  }
}

/**
 *  Function to handle the submit from the add_fields form
 */
function opencalais_add_fields_submit($form, &$form_state){
  if (!isset($form_state['opencalais_processed']) || !$form_state['opencalais_processed']) {

    $content_type = $form_state['values']['content_type'];

    $to_add = array_filter($form_state['values']['config']['entities'], '_opencalais_filter');
    $to_rem = array_filter($form_state['values']['config']['entities'], '_opencalais_delete_filter');

    opencalais_create_fields($content_type, $to_add);
    opencalais_remove_fields($content_type, $to_rem);
    
    //I'm slightly worried about batch operations causing a race condition here - we'll have to see
    $settings = variable_get('opencalais_settings', array());
    $settings['autotagging'][$content_type] = $form_state['values'][$content_type.'_autotagging'];
    $settings['node_fields'][$content_type] = $form_state['values'][$content_type.'_node_fields'];
    variable_set('opencalais_settings', $settings);
  }
}



/**
 *  Retrieve a list of extra fields to apply to a vocabulary
 */
function opencalais_get_extra_fields($vocab){
  $geo = array('City', 'Country', 'ProvinceOrState');
  $company = array('Company');
  $product = array('Product');
  
  if (in_array($vocab, $geo)) {
    return array('Latitude', 'Longitude', 'ContainedByState', 'ContainedByCounty');
  } else if (in_array($vocab, $company)) {
    return array('Ticker', 'LegalName');
  } else if (in_array($vocab, $product)) {
    return array();
  }
}

/**
 *  Implements hook_cron - allows for bulk processing
 */
function opencalais_cron(){
  module_load_include('inc', 'opencalais', 'opencalais.bulk');
  opencalais_queue_cron();
}

/**
 *  Implements hook_opencalais_tags_alter()
 *
 *  calls our apply filters function to alter the tags
 */
function opencalais_opencalais_tags_alter(&$data){
  module_load_include('inc', 'opencalais', 'opencalais.filters');
  opencalais_filters_apply_filters($data['tags'], $data['content_type']);
}
