<?php
/**
 * @file
 * The Tripal Web Service Module
 */

/**
 * @defgroup tripal_ws Tripal Web Services Module
 * @ingroup tripal
 * @{
 * The Tripal Web Services module provides functionality for managing RESTful
 * web services based on the W3C Hydra standard.
 * @}
 */

/**
 * @defgroup tripal_api Tripal API
 * @ingroup tripal
 * @{
 * Tripal provides an application programming interface (API) to support
 * customizations and creation of new extensions.
 * @
 */
require_once  "api/tripal_ws.api.inc";
require_once  "includes/tripal_ws.field_storage.inc";
require_once  "includes/tripal_ws.fields.inc";
require_once  "includes/TripalWebService.inc";
require_once  "includes/TripalWebServiceResource.inc";
require_once  "includes/TripalWebServiceCollection.inc";

// Web Services Fields
require_once "includes/TripalFields/WebServicesField.inc";
require_once "includes/TripalFields/WebServicesFieldWidget.inc";
require_once "includes/TripalFields/WebServicesFieldFormatter.inc";

/**
 * Implements hook_init()
 */
function tripal_ws_init() {
  global $base_url;

  $api_url = $base_url . '/web-sevices/';

  $vocab = tripal_get_vocabulary_details('hydra');

  // Following the WC3 Hydra documentation, we want to add  LINK to the header
  // of the site that indicates where the API documentation can be found.
  // This allows a hydra-enabled client to discover the API and use it.
  $attributes = array(
    'rel' => $vocab['sw_url'] . 'apiDocumentation',
    'href' => $api_url . '/doc/v0.1',
  );
  drupal_add_html_head_link($attributes, $header = FALSE);
}

/**
 * Implements hook_menu().
 * Defines all menu items needed by Tripal Core
 *
 * @ingroup tripal_ws
 */
function tripal_ws_menu() {

  // Web Services API callbacks.
  $items['web-services'] = array(
    'title' => 'Tripal Web Services API',
    'page callback' => 'tripal_ws_get_services',
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
  );

  $items['remote/%/%/%/%'] = array(
    'page callback' => 'tripal_ws_load_remote_entity',
    'page arguments' => array(1, 2, 3, 4),
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
  );

  // Tripal Web Services setting groups
  $items['admin/tripal/storage/ws'] = array(
    'title' => 'Remote Tripal Sites',
    'description' => t("Create mashups of content using data from this site and remote Tripal sites."),
    'weight' => 20,
    'page callback' => 'system_admin_menu_block_page',
    'access arguments' => array('administer tripal'),
    'file' => 'system.admin.inc',
    'file path' => drupal_get_path('module', 'system'),
  );
  $items['admin/tripal/storage/ws/tripal_sites'] = array(
    'title' => 'Configuration',
    'description' => t('Provides information about other Tripal sites.
        This allows data exchange and communication betwen Tripal
        enabled sites through the web services.'),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('tripal_ws_tripal_sites_form'),
    'access arguments' => array('administer tripal'),
    'type' => MENU_NORMAL_ITEM,
    'weight' => 0,
    'file' => 'includes/tripal_ws.admin.inc',
    'file path' => drupal_get_path('module', 'tripal_ws'),
  );
  $items['admin/tripal/storage/ws/tripal_sites/edit'] = array(
    'title' => 'Add Tripal Site',
    'description' => 'Add a Tripal site',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('tripal_ws_tripal_sites_edit_form'),
    'access arguments' => array('administer tripal'),
    'file' =>  'includes/tripal_ws.admin.inc',
    'file path' => drupal_get_path('module', 'tripal_ws'),
    'type' => MENU_LOCAL_ACTION,
    'weight' => 2
  );
  $items['admin/tripal/storage/ws/tripal_sites/remove/%'] = array(
    'title' => 'Remove Tripal Site',
    'description' => 'Remove a Tripal site',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('tripal_ws_tripal_sites_remove_form', 6),
    'access arguments' => array('administer tripal'),
    'file' =>  'includes/tripal_ws.admin.inc',
    'file path' => drupal_get_path('module', 'tripal_ws'),
    'type' => MENU_CALLBACK,
    'weight' => 2
  );
  return $items;
}

/**
 * The callback function for all RESTful web services.
 *
 */
function tripal_ws_get_services() {
  global $base_url;
  $service_path = $base_url . '/web-services';

  // This should go out as ld+json
  drupal_add_http_header('Content-Type', 'application/ld+json');

  // Add a link header for the vocabulary service so that clients
  // know where to find the docs.
  tripal_load_include_web_service_class('TripalDocService_v0_1');
  $service = new TripalDocService_v0_1($service_path);
  $vocab = tripal_get_vocabulary_details('hydra');
  drupal_add_http_header('Link', '<' . $service->getServicePath() . '>; rel="' . $vocab['sw_url'] . 'apiDocumentation"');
  drupal_add_http_header('Cache-Control', "no-cache");

  try {
    $ws_path = func_get_args();
    $args = $_GET;
    unset($args['q']);

    // The web services should never be cached.
    drupal_page_is_cacheable(FALSE);

    // The Tripal web services bath will be:
    // [base_path]/web-services/[service name]/v[major_version].[minor_version]
    $matches = array();
    $service = '';
    $major_version = '';
    $minor_version = '';
    $list_services = FALSE;

    // If there is no path then we should list all of the services available.
    if (empty($ws_path)) {
      tripal_ws_list_services();
      return;
    }
    // A service path will have the service name in $ws_path[0] and the
    // version in $ws_path[1].  If we check that the version is correctly
    // formatted then we can look for the service class and invoke it.
    else if (preg_match("/^v(\d+)\.(\d+)$/", $ws_path[1], $matches)) {
      $service_type = $ws_path[0];
      $major_version = $matches[1];
      $minor_version = $matches[2];
      $service_version = 'v' . $major_version . '.' . $minor_version;
    }
    // If the URL doesn't match then return not found.
    else {
      throw new Exception("Unsupported service URL: '" . $ws_path[1] . "'");
    }

    // Get the service that matches the service_name
    $service = NULL;
    $services = tripal_get_web_services();
    foreach ($services as $service_class) {
      tripal_load_include_web_service_class($service_class);
      if ($service_class::$type == $service_type) {
        $service = new $service_class($service_path);
        if ($service->getVersion() == $service_version) {
          break;
        }
        $service = NULL;
      }
    }
    // If a service was not provided then return an error.
    if (!$service) {
      throw new Exception('The service type, "' . $service_type . '", is not available');
    }
    // Adjust the path to remove the service type and the version.
    $adj_path = $ws_path;
    array_shift($adj_path);
    array_shift($adj_path);

    // Now call the service to handle the request.
    $service->setPath($adj_path);
    $service->setParams($args);
    $service->handleRequest();
    $response = $service->getResponse();
    print drupal_json_encode($response);

  }
  catch (Exception $e) {
    $service = new TripalWebService($service_path);
    $service->setError($e->getMessage());
    $response = $service->getResponse();
    print drupal_json_encode($response);
  }
}

/**
 * Generates the list of services as the "home page" for Tripal web services.
 */
function tripal_ws_list_services() {
  global $base_url;
  $base_path = $base_url . '/web-services';

  // Create an instance of the TriaplWebService class and use it to build
  // the entry point for the web serivces.
  $service = new TripalWebService($base_path);

  // Get the list of web service classes.
  $services = tripal_get_web_services();

  // Create the parent resource which is a collection.
  $resource = new TripalWebServiceResource($base_path);

  // Add the vocabulary to the context.
  tripal_load_include_web_service_class('TripalDocService_v0_1');
  $service = new TripalDocService_v0_1($base_path);
  $resource->addContextItem('vocab', $service->getServicePath() . '#');
  $resource->addContextItem('EntryPoint', 'vocab:EntryPoint');
  $resource->setType('EntryPoint');

  // Now add the services as properties.
  foreach ($services as $service_class) {
    tripal_load_include_web_service_class($service_class);
    if ($service_class == 'TripalDocService_v0_1') {
      continue;
    }
    $service = new $service_class($base_path);
    $resource->addContextItem($service_class::$type, array(
      '@id' => 'vocab:EntryPoint/' . $service_class::$type,
      '@type' => '@id',
    ));
    $resource->addProperty($service_class::$type, $service->getServicePath());
  }

  $service->setResource($resource);
  $response = $service->getResponse();
  print drupal_json_encode($response);



}
/**
 * The callback function for all RESTful web services.
 *
 */
function tripal_ws_services() {
  $ws_path = func_get_args();
  $params = $_GET;
  unset($params['q']);

  // The web services should never be cached.
  drupal_page_is_cacheable(FALSE);

  // Using the provided version number, determine which web services
  // verion to call.
  $version = array_shift($ws_path);
  if ($version and preg_match('/v\d+\.\d+/', $version)) {

    $api_url = 'ws/' . $version;

    // Add the file with the appropriate web services.
    module_load_include('inc', 'tripal_ws', 'includes/tripal_ws.rest_' . $version);
    $version = preg_replace('/\./', '_', $version);
    $function = 'tripal_ws_services_' . $version;
    $response = array();
    if (function_exists($function)) {
      $response = $function($api_url, $ws_path, $params);
    }
  }
  else {
    // TODO: What do we do if no version is provided?
  }

  drupal_add_http_header('Content-Type', 'application/ld+json');
  print drupal_json_encode($response);
}

/**
 *
 * @param $site_id
 * @param $api_version
 * @param $ctype
 * @param $id
 *
 * @return
 */
function tripal_ws_load_remote_entity($site_id, $api_version, $ctype, $id) {

  // Get the content type on this site
  $bundle = tripal_load_bundle_entity(array('label' => $ctype));
  $term = entity_load('TripalTerm', array('id' => $bundle->term_id));
  $term = reset($term);
  $vocab = $term->vocab;

  $query = db_select('tripal_sites', 'ts');
  $query->fields('ts');
  $query->condition('id', $site_id);
  $site = $query->execute()->fetchObject();

  if (!$site) {
    return 'Could not find specified site.';
  }

  // Get the content from the web services of the remote site.
  $url = $site->url . "/ws/v0.1/content/" . $ctype . "/" . $id;
  $json = file_get_contents($url);
  $response = json_decode($json, TRUE);

  // Set the title for this page to match the title provided.
  drupal_set_title($response['label']);

  // Attribute this data to the proper source.
  $source_url = l($response['label'], $response['ItemPage'], array('attributes' => array('target' => '_blank')));
  $content = '<div><strong>Source:</strong> ' . $site->name . ': ' . $source_url . '</div>';

  // Fake an entity so we can display this content using the same
  // entity type on this site.
  $entity = new TripalEntity(array(), 'TripalEntity');
  $entity->id = 807;
  $entity->type = 'TripalEntity';
  $entity->bundle = $bundle->name;
  $entity->term_id = $term->id;
  $entity->title = $response['label'];
  $entity->uid = 1;
  $entity->status = 1;

  // Get the fields and create a list of those that are attached to the bundle.
  $fields = field_info_fields();
  $my_fields = array();
  foreach ($fields as $field) {
    if (isset($field['bundles']['TripalEntity'])) {
      foreach ($field['bundles']['TripalEntity'] as $bundle_name) {
        if ($bundle_name == $bundle->name) {
          $my_fields[] = $field;
        }
      }
    }
  }

  // Add in the value for the 'content_type' field.
  $entity->content_type = array();
  $entity->content_type['und'][0]['value'] = $bundle->label;

  // For each field we know about that should be attached to our bundle,
  // see if we can find a corresponding entry in the results returned from
  // the web service call. If so, then add the field to our fake entity.
  foreach ($my_fields as $field) {
    // Get the semantic web term for this field.
    $field_name = $field['field_name'];
    $settings = $field['settings'];

    // If the field does not have a semantic web mapping, then skip it.
    if (!isset($settings['semantic_web'])) {
      continue;
    }

    // Convert the term into it's db and accession elements and look it up
    // for more details.
    list($vocabulary, $accession) = explode(':', $settings['semantic_web']);
    $term = tripal_get_term_details($vocabulary, $accession);

    // Convert the term to lowercase and remove spaces so we can compare
    // correctly.
    $term_name = strtolower(preg_replace('/ /', '_', $term['name']));

    // TODO: check for the term in the response makes the assumption
    // that the term is the same on both sides. This may not be true. The
    // acutal vocab and accession for both terms should be compared.
    if (isset($response[$term_name])) {

      // If this field is of type '@id' then this links out to another
      // URL where that information can be retrieved. We'll have to
      // handle that separately.
      if (isset($response['@context'][$term_name]['@type']) and
          $response['@context'][$term_name]['@type'] == '@id') {
        $subquery = json_decode(file_get_contents($response[$term_name]), TRUE);

        // If the result is a collection then we want to add each value with
        // it's own delta value.
        if (array_key_exists('@type', $subquery) and $subquery['@type'] == 'Collection') {
          $i = 0;
          $f = array();
          foreach ($subquery['member'] as $member) {
            $f['und'][$i]['value'] = $member;
            $i++;
          }
          $entity->$field_name = $f;
        }
        // If the result is not a collection then just add it.
        else {
          unset($subquery['@context']);
          unset($subquery['@id']);
          $f = array();
          $f['und'][0]['value'] = $subquery;
          $entity->$field_name = $f;
        }
      }
      // For all fields that are currently attached, add the field and
      // value to the entity.
      else {
        $f = array();
        $f['und'][0]['value'] = $response[$term_name];
        $entity->$field_name = $f;
      }
    }
  }

  // Generate the View for this entity
  $entities = array();
  $entities[] = $entity;
  $view = entity_view('TripalEntity', $entities);
  $content .= drupal_render($view['TripalEntity'][807]);

  return $content;

}

function tripal_ws_form_field_ui_field_edit_form_alter(&$form, &$form_state, $form_id) {
  // Don't let the user change the cardinality of web services fields
  if ($form['#instance']['entity_type'] == 'TripalEntity') {
    if ($form['#field']['storage']['type'] == 'field_tripal_ws_storage') {
      $form['field']['cardinality']['#access'] = FALSE;
      $form['instance']['required']['#access'] = FALSE;
    }
  }
}

/*
* Returns the decoded json data for a specific field.
*/
function tripal_ws_remote_data_single_field_pull($field, $entity_url){
  $options = array();
  $full_url = $entity_url . '/' . $field;
  $data = drupal_http_request($full_url, $options);
  if(!empty($data)){
    $data = drupal_json_decode($data->data);
  }
  return $data;
}


/**
 * Implements hook_entity_info_alter()
 *
 * Add the web services display as a view mode.
 */
function tripal_ws_entity_info_alter(&$entity_info) {

  // Set the controller class for nodes to an alternate implementation of the
  // DrupalEntityController interface.
  $entity_info['TripalEntity']['view modes']  += array(
    'tripal_ws' => array(
      'label' => t('Tripal Web Services'),
      'custom settings' => FALSE,
    ),
  );
}