<?php

namespace Drupal\straker_translate;

use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\SortArray;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\RevisionLogInterface;
use Drupal\Core\Entity\RevisionableEntityBundleInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\straker_translate\Entity\StrakerTranslateContentMetadata;
use Drupal\straker_translate\Exception\StrakerTranslateApiException;
use Drupal\straker_translate\Exception\StrakerTranslateContentEntityStorageException;
use Drupal\straker_translate\Exception\StrakerTranslateDocumentAlreadyCompletedException;
use Drupal\straker_translate\Exception\StrakerTranslateDocumentAlreadyUploaded;
use Drupal\straker_translate\Exception\StrakerTranslateDocumentArchivedException;
use Drupal\straker_translate\Exception\StrakerTranslateDocumentLockedException;
use Drupal\straker_translate\Exception\StrakerTranslateDocumentNotFoundException;
use Drupal\straker_translate\Exception\StrakerTranslatePaymentRequiredException;
use Drupal\straker_translate\Exception\StrakerTranslateProcessedWordsLimitException;
use Drupal\node\NodeInterface;

/**
 * Service for managing Straker Translate content translations.
 */
class StrakerTranslateContentTranslationService implements StrakerTranslateContentTranslationServiceInterface {

  use StringTranslationTrait;

  /**
   * The Straker Translate interface.
   *
   * @var \Drupal\straker_translate\StrakerTranslateInterface
   */
  protected $straker_translate;

  /**
   * The language-locale mapper.
   *
   * @var \Drupal\straker_translate\LanguageLocaleMapperInterface
   */
  protected $languageLocaleMapper;

  /**
   * The Straker Translate configuration service.
   *
   * @var \Drupal\straker_translate\StrakerTranslateConfigurationServiceInterface
   */
  protected $straker_translateConfiguration;

  /**
   * The Straker Translate configuration translation service.
   *
   * @var \Drupal\straker_translate\StrakerTranslateConfigTranslationServiceInterface
   */
  protected $straker_translateConfigTranslation;

  /**
   * The entity manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The entity field manager.
   *
   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
   */
  protected $entityFieldManager;

  /**
   * The language manager.
   *
   * @var \Drupal\Core\Language\LanguageManagerInterface
   */
  protected $languageManager;

  /**
   * The database connection.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $connection;

  /**
   * The content entity revision resolver from where to extract the data from.
   *
   * @var \Drupal\straker_translate\StrakerTranslateContentTranslationEntityRevisionResolverInterface
   */
  protected $contentRevisionResolver;

  /**
   * Constructs a new StrakerTranslateContentTranslationService object.
   *
   * @param \Drupal\straker_translate\StrakerTranslateInterface $straker_translate
   *   An straker_translate object.
   * @param \Drupal\straker_translate\LanguageLocaleMapperInterface $language_locale_mapper
   *   The language-locale mapper.
   * @param \Drupal\straker_translate\StrakerTranslateConfigurationServiceInterface $straker_translate_configuration
   *   The Straker Translate configuration service.
   * @param \Drupal\straker_translate\StrakerTranslateConfigTranslationServiceInterface $straker_translate_config_translation
   *   The Straker Translate config translation service.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   An entity manager object.
   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
   *   The language manager.
   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
   *   The entity field manager.
   * @param \Drupal\Core\Database\Connection $connection
   *   The database connection object.
   * @param \Drupal\straker_translate\StrakerTranslateContentTranslationEntityRevisionResolverInterface $content_revision_resolver
   *   The content revision resolver.
   */
  public function __construct(StrakerTranslateInterface $straker_translate, LanguageLocaleMapperInterface $language_locale_mapper, StrakerTranslateConfigurationServiceInterface $straker_translate_configuration, StrakerTranslateConfigTranslationServiceInterface $straker_translate_config_translation, EntityTypeManagerInterface $entity_type_manager, LanguageManagerInterface $language_manager, EntityFieldManagerInterface $entity_field_manager, Connection $connection, StrakerTranslateContentTranslationEntityRevisionResolverInterface $content_revision_resolver) {
    $this->straker_translate = $straker_translate;
    $this->languageLocaleMapper = $language_locale_mapper;
    $this->straker_translateConfiguration = $straker_translate_configuration;
    $this->straker_translateConfigTranslation = $straker_translate_config_translation;
    $this->entityTypeManager = $entity_type_manager;
    $this->languageManager = $language_manager;
    $this->entityFieldManager = $entity_field_manager;
    $this->connection = $connection;
    $this->contentRevisionResolver = $content_revision_resolver;
  }

  /**
   * {@inheritdoc}
   */
  public function checkSourceStatus(ContentEntityInterface &$entity) {
    $profile = $this->straker_translateConfiguration->getEntityProfile($entity);
    if ($profile->id() === StrakerTranslate::PROFILE_DISABLED || $this->getSourceStatus($entity) === StrakerTranslate::STATUS_CANCELLED) {
      return FALSE;
    }
    if (!in_array($this->getSourceStatus($entity), [StrakerTranslate::STATUS_READY, StrakerTranslate::STATUS_PROCESSING, StrakerTranslate::STATUS_ERROR])) {
      throw new StrakerTranslateDocumentAlreadyUploaded('The document has already been uploaded and is being processed or is current.', 0);
    }
    $document_id = $this->getDocumentId($entity);
    if ($document_id) {
      // Document has successfully imported.
      try {
        $response = $this->straker_translate->getDocumentStatus($document_id);
      }
      catch (StrakerTranslateDocumentLockedException $exception) {
        throw $exception;
      }
      catch (StrakerTranslateDocumentNotFoundException $exception) {
        throw $exception;
      }
      catch (StrakerTranslateDocumentArchivedException $exception) {
        $this->deleteMetadata($entity);
        throw $exception;
      }
      catch (StrakerTranslatePaymentRequiredException $exception) {
        throw $exception;
      }
      catch (StrakerTranslateApiException $exception) {
        throw $exception;
      }
      if ($response["data"]["status"] == 'COMPLETED') {
        $this->setSourceStatus($entity, StrakerTranslate::STATUS_CURRENT);
        $this->setTranslatedTargetStatus($entity, $response);
        return TRUE;
      }
      if ($response["data"]["status"] == 'FAILED' || $response["data"]["status"] == 'UNSUCCESSFUL') {
        $this->setSourceStatus($entity, StrakerTranslate::STATUS_ERROR);
        $this->setTargetStatuses($entity, StrakerTranslate::STATUS_ERROR);
        throw new StrakerTranslateApiException("The document status is failed. Please check in Straker");
      }
      if (!isset($response["data"]["status"])) {
        $this->setSourceStatus($entity, StrakerTranslate::STATUS_ERROR);
        $this->setTargetStatuses($entity, StrakerTranslate::STATUS_ERROR);
        throw new StrakerTranslateApiException("The document status is failed. Please check in Straker");
      }
      else {
        $this->setSourceStatus($entity, StrakerTranslate::STATUS_PROCESSING);
        $this->setTargetStatuses($entity, StrakerTranslate::STATUS_PENDING);
      }
    }
    if ($this->getSourceStatus($entity) == StrakerTranslate::STATUS_DISABLED) {
      $this->setTargetStatuses($entity, StrakerTranslate::STATUS_DISABLED);
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function getSourceStatus(ContentEntityInterface &$entity) {
    $source_language = LanguageInterface::LANGCODE_NOT_SPECIFIED;
    /** @var \Drupal\straker_translate\Entity\StrakerTranslateContentMetadata $metadata */
    $metadata = $entity->straker_translate_metadata ? $entity->straker_translate_metadata->entity : NULL;
    if ($metadata !== NULL && $metadata->translation_source && $metadata->translation_source->value !== NULL) {
      $source_language = $metadata->translation_source->value;
    }
    if ($source_language == LanguageInterface::LANGCODE_NOT_SPECIFIED) {
      $source_language = $entity->getUntranslated()->language()->getId();
    }
    return $this->getTargetStatus($entity, $source_language);
  }

  /**
   * {@inheritdoc}
   */
  public function setSourceStatus(ContentEntityInterface &$entity, $status) {
    /** @var \Drupal\straker_translate\Entity\StrakerTranslateContentMetadata $metadata */
    $metadata = $entity->straker_translate_metadata ? $entity->straker_translate_metadata->entity : NULL;
    $source_language = $metadata->translation_source->value;
    if ($source_language == LanguageInterface::LANGCODE_NOT_SPECIFIED || $source_language == NULL) {
      $source_language = $entity->getUntranslated()->language()->getId();
    }
    return $this->setTargetStatus($entity, $source_language, $status);
  }

  /**
   * Clear the target statuses.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   */
  protected function clearTargetStatuses(ContentEntityInterface &$entity) {
    $profile = $this->straker_translateConfiguration->getEntityProfile($entity);
    if ($profile->id() === StrakerTranslate::PROFILE_DISABLED || $this->getSourceStatus($entity) === StrakerTranslate::STATUS_CANCELLED) {
      return FALSE;
    }
    // Clear the target statuses. As we save the source status with the target,
    // we need to keep that one.
    $source_status = $this->getSourceStatus($entity);

    /** @var \Drupal\straker_translate\Entity\StrakerTranslateContentMetadata $metadata */
    $metadata = $entity->straker_translate_metadata ? $entity->straker_translate_metadata->entity : NULL;
    if ($metadata->hasField('translation_status') && count($metadata->translation_status) > 0) {
      $metadata->translation_status = NULL;
    }
    $this->setTargetStatus($entity, $entity->getUntranslated()->language()->getId(), $source_status);
  }

  /**
   * {@inheritdoc}
   */
  public function checkTargetStatuses(ContentEntityInterface &$entity) {
    /** @var \Drupal\straker_translate\StrakerTranslateProfileInterface $profile */
    $profile = $this->straker_translateConfiguration->getEntityProfile($entity);
    if ($profile->id() === StrakerTranslate::PROFILE_DISABLED || $this->getSourceStatus($entity) === StrakerTranslate::STATUS_CANCELLED) {
      return FALSE;
    }
    $document_id = $this->getDocumentId($entity);
    try {
      $translation_statuses = $this->straker_translate->getDocumentTranslationStatuses($document_id);
    }
    catch (StrakerTranslateDocumentNotFoundException $exception) {
      throw $exception;
    }
    $source_status = $this->getSourceStatus($entity);

    $statuses = [];
    $languages = $this->languageManager->getLanguages();
    foreach ($languages as $language) {
      $statuses[$language->getId()] = $this->getTargetStatus($entity, $language->getId());
    }

    // Let's reset all statuses, but keep the source one.
    $this->clearTargetStatuses($entity);

    foreach ($translation_statuses as $straker_translate_locale => $progress) {
      $drupal_language = $this->languageLocaleMapper->getConfigurableLanguageForLocale($straker_translate_locale);
      if ($drupal_language == NULL) {
        // Language existing in Verify, but not configured on Drupal.
        continue;
      }
      $langcode = $drupal_language->id();
      $current_target_status = $statuses[$langcode];
      if (in_array($current_target_status, [StrakerTranslate::STATUS_UNTRACKED, StrakerTranslate::STATUS_DISABLED, StrakerTranslate::STATUS_EDITED, StrakerTranslate::STATUS_REQUEST, StrakerTranslate::STATUS_NONE, StrakerTranslate::STATUS_READY, StrakerTranslate::STATUS_PENDING, StrakerTranslate::STATUS_CANCELLED, NULL])) {
        if ($progress === StrakerTranslate::STATUS_CANCELLED) {
          $this->setTargetStatus($entity, $langcode, StrakerTranslate::STATUS_CANCELLED);
        }
        elseif ($progress === StrakerTranslate::PROGRESS_COMPLETE) {
          $this->setTargetStatus($entity, $langcode, StrakerTranslate::STATUS_READY);
        }
        else {
          if (!$profile->hasDisabledTarget($langcode)) {
            $this->setTargetStatus($entity, $langcode, StrakerTranslate::STATUS_PENDING);
          }
          else {
            $this->setTargetStatus($entity, $langcode, StrakerTranslate::STATUS_DISABLED);
          }
        }
      }
      if ($source_status !== StrakerTranslate::STATUS_CURRENT && $statuses[$langcode] === StrakerTranslate::STATUS_EDITED && $langcode !== $entity->getUntranslated()->language()->getId()) {
        $this->setTargetStatus($entity, $langcode, StrakerTranslate::STATUS_EDITED);
      }
      if ($source_status === StrakerTranslate::STATUS_CURRENT && $statuses[$langcode] === StrakerTranslate::STATUS_CURRENT && $langcode !== $entity->getUntranslated()->language()->getId()) {
        $this->setTargetStatus($entity, $langcode, StrakerTranslate::STATUS_CURRENT);
      }
      if ($profile->hasDisabledTarget($langcode)) {
        $this->setTargetStatus($entity, $langcode, StrakerTranslate::STATUS_DISABLED);
      }
    }
    if ($this->getSourceStatus($entity) == StrakerTranslate::STATUS_DISABLED) {
      $this->setTargetStatuses($entity, StrakerTranslate::STATUS_DISABLED);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function checkTargetStatus(ContentEntityInterface &$entity, $langcode) {
    /** @var \Drupal\straker_translate\StrakerTranslateProfileInterface $profile */
    $profile = $this->straker_translateConfiguration->getEntityProfile($entity);
    if ($profile->id() === StrakerTranslate::PROFILE_DISABLED || $this->getSourceStatus($entity) === StrakerTranslate::STATUS_CANCELLED
        || $profile->hasDisabledTarget($langcode)) {
      return FALSE;
    }
    $current_status = $this->getTargetStatus($entity, $langcode);
    $locale = $this->languageLocaleMapper->getLocaleForLangcode($langcode);
    $source_status = $this->getSourceStatus($entity);
    $document_id = $this->getDocumentId($entity);
    if ($langcode !== $entity->getUntranslated()->language()->getId()) {
      if (($current_status == StrakerTranslate::STATUS_PENDING ||
      $current_status == StrakerTranslate::STATUS_EDITED) &&
      $source_status !== StrakerTranslate::STATUS_EDITED) {
        try {
          $translation_status = $this->straker_translate->getDocumentTranslationStatus($document_id, $locale);
        }
        catch (StrakerTranslateDocumentNotFoundException $exception) {
          throw $exception;
        }
        if ($translation_status === StrakerTranslate::STATUS_CANCELLED) {
          $this->setTargetStatus($entity, $langcode, StrakerTranslate::STATUS_CANCELLED);
        }
        elseif ($translation_status === TRUE) {
          $current_status = StrakerTranslate::STATUS_READY;
          $this->setTargetStatus($entity, $langcode, $current_status);
        }
        // We may not be ready, but some phases must be complete. Let's try to
        // download data, and if there is anything, we can assume a phase is
        // completed.
        // @todo Instead of downloading would be nice if we could check phases.
        elseif ($this->straker_translate->downloadDocument($document_id, $locale)) {
          // @todo Set Status to STATUS_READY_INTERIM when that status is
          // available. See ticket: https://www.drupal.org/node/2850548
        }
      }
      elseif ($current_status == StrakerTranslate::STATUS_REQUEST || $current_status == StrakerTranslate::STATUS_UNTRACKED) {
        try {
          $translation_status = $this->straker_translate->getDocumentTranslationStatus($document_id, $locale);
        }
        catch (StrakerTranslateDocumentNotFoundException $exception) {
          throw $exception;
        }
        if ($translation_status === TRUE) {
          $current_status = StrakerTranslate::STATUS_READY;
          $this->setTargetStatus($entity, $langcode, $current_status);
        }
        elseif ($translation_status !== FALSE) {
          $current_status = StrakerTranslate::STATUS_PENDING;
          $this->setTargetStatus($entity, $langcode, $current_status);
        }
        // Elseif ($this->straker_translate->downloadDocument($document_id, $locale)) {
        //   // @todo Set Status to STATUS_READY_INTERIM when that status is
        //   // available. See ticket: https://www.drupal.org/node/2850548
        // }
      }
    }
    if ($this->getSourceStatus($entity) == StrakerTranslate::STATUS_DISABLED) {
      $this->setTargetStatuses($entity, StrakerTranslate::STATUS_DISABLED);
    }
    return $current_status;
  }

  /**
   * {@inheritdoc}
   */
  public function getTargetStatus(ContentEntityInterface &$entity, $langcode) {
    $status = StrakerTranslate::STATUS_UNTRACKED;
    $statuses = $this->getTargetStatuses($entity);
    if (isset($statuses[$langcode])) {
      $status = $statuses[$langcode];
    }
    return $status;
  }

  /**
   * {@inheritdoc}
   *
   * @throws \Exception
   */
  public function getTargetStatuses(ContentEntityInterface &$entity) {
    $statuses = [];
    /** @var \Drupal\straker_translate\Entity\StrakerTranslateContentMetadata $metadata */
    $metadata = $entity->straker_translate_metadata ? $entity->straker_translate_metadata->entity : NULL;
    if ($metadata !== NULL && count($metadata->translation_status) > 0) {
      /** @var \Drupal\Core\Entity\ContentEntityBase $translation_status */
      $translation_status = $metadata->get('translation_status');
      foreach ($translation_status->getIterator() as $delta => $value) {
        $statuses[$value->language] = $value->value;
      }
    }
    return $statuses;
  }

  /**
   * {@inheritdoc}
   */
  public function setTargetStatus(ContentEntityInterface &$entity, $langcode, $status, $save = TRUE) {
    $set = FALSE;

    if (!$entity->straker_translate_metadata->entity) {
      $entity->set('straker_translate_metadata', StrakerTranslateContentMetadata::loadByTargetId($entity->getEntityTypeId(), $entity->id()));
    }
    /** @var \Drupal\straker_translate\Entity\StrakerTranslateContentMetadata $metadata */
    $metadata = $entity->straker_translate_metadata ? $entity->straker_translate_metadata->entity : NULL;
    if ($metadata->hasField('translation_status') && count($metadata->translation_status) > 0) {
      /** @var \Drupal\Core\Entity\ContentEntityBase $translation_status */
      $translation_status = $metadata->get('translation_status');
      foreach ($translation_status->getIterator() as $delta => $value) {
        if ($value->language == $langcode) {
          $value->value = $status;
          $set = TRUE;
        }
      }
    }
    if (!$set && $metadata->hasField('translation_status')) {
      $metadata->translation_status->appendItem(['language' => $langcode, 'value' => $status]);
      $set = TRUE;
    }
    if ($set) {
      $entity->straker_translate_processed = TRUE;
      $metadata->save();
    }
    return $entity;
  }

  /**
   * {@inheritdoc}
   */
  public function setTargetStatuses(ContentEntityInterface &$entity, $status): void {
    $target_languages = $this->languageManager->getLanguages();
    $entity_langcode = $entity->getUntranslated()->language()->getId();

    foreach ($target_languages as $langcode => $language) {
      if ($langcode != $entity_langcode && $current_status = $this->getTargetStatus($entity, $langcode)) {
        if ($current_status === StrakerTranslate::STATUS_PENDING && $status === StrakerTranslate::STATUS_REQUEST) {
          // Don't allow to pass from pending to request. We have been already
          // requested this one.
          continue;
        }
        if ($current_status === StrakerTranslate::STATUS_LOCKED && $status !== StrakerTranslate::STATUS_CURRENT) {
          // Don't allow to pass from pending to request. We have been already
          // requested this one.
          continue;
        }
        if ($current_status == $status) {
          continue;
        }
        if ($current_status != StrakerTranslate::STATUS_EDITED && $current_status !== StrakerTranslate::STATUS_CURRENT) {
          $this->setTargetStatus($entity, $langcode, $status);
        }
        elseif ($current_status == StrakerTranslate::STATUS_EDITED && in_array($status, [StrakerTranslate::STATUS_CURRENT, StrakerTranslate::STATUS_PENDING])) {
          $this->setTargetStatus($entity, $langcode, $status);
        }
        if (in_array($status, [StrakerTranslate::STATUS_ARCHIVED, StrakerTranslate::STATUS_DELETED, StrakerTranslate::STATUS_CANCELLED, StrakerTranslate::STATUS_DISABLED])) {
          $this->setTargetStatus($entity, $langcode, $status);
        }
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function markTranslationsAsDirty(ContentEntityInterface &$entity) {
    $profile = $this->straker_translateConfiguration->getEntityProfile($entity);
    if ($profile->id() === StrakerTranslate::PROFILE_DISABLED || $this->getSourceStatus($entity) === StrakerTranslate::STATUS_CANCELLED) {
      return FALSE;
    }
    $target_languages = $this->languageManager->getLanguages();
    $entity_langcode = $entity->getUntranslated()->language()->getId();

    // Only mark as out of date the current ones.
    $to_change = [
      StrakerTranslate::STATUS_CURRENT,
      // StrakerTranslate::STATUS_PENDING,
      // StrakerTranslate::STATUS_INTERMEDIATE,
      // StrakerTranslate::STATUS_READY,.
    ];

    foreach ($target_languages as $langcode => $language) {
      if ($langcode != $entity_langcode && $current_status = $this->getTargetStatus($entity, $langcode)) {
        if (in_array($current_status, $to_change)) {
          $this->setTargetStatus($entity, $langcode, StrakerTranslate::STATUS_PENDING);
        }
      }
    }
    return $entity;
  }

  /**
   * {@inheritdoc}
   */
  public function getDocumentId(ContentEntityInterface &$entity) {
    $doc_id = NULL;
    $metadata = $entity->hasField('straker_translate_metadata') ? $entity->straker_translate_metadata->entity : NULL;
    if ($metadata !== NULL && $metadata->document_id) {
      $doc_id = $metadata->document_id->value;
    }
    return $doc_id;
  }

  /**
   * {@inheritdoc}
   */
  public function setDocumentId(ContentEntityInterface &$entity, $doc_id) {
    if ($entity->straker_translate_metadata->entity === NULL) {
      $entity->set('straker_translate_metadata', StrakerTranslateContentMetadata::loadByTargetId($entity->getEntityTypeId(), $entity->id()));
    }
    $entity->straker_translate_processed = TRUE;
    $entity->straker_translate_metadata->entity->setDocumentId($doc_id);
    $entity->straker_translate_metadata->entity->save();

    return $entity;
  }

  /**
   * {@inheritdoc}
   */
  public function getSourceLocale(ContentEntityInterface &$entity) {
    $source_language = $entity->getUntranslated()->language()->getId();
    return $this->languageLocaleMapper->getLocaleForLangcode($source_language);
  }

  /**
   * {@inheritdoc}
   */
  public function getSourceData(ContentEntityInterface &$entity, &$visited = [], string $revision_mode = StrakerTranslateContentTranslationEntityRevisionResolver::RESOLVE_LATEST_TRANSLATION_AFFECTED) {
    $source_entity = $this->contentRevisionResolver->resolve($entity, $revision_mode);
    $isParentEntity = count($visited) === 0;
    $visited[$entity->bundle()][] = $entity->id();
    $entity_type = $entity->getEntityType();
    $field_definitions = $this->entityFieldManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle());
    $storage_definitions = $entity_type instanceof ContentEntityTypeInterface ? $this->entityFieldManager->getFieldStorageDefinitions($entity_type->id()) : [];
    $translatable_fields = [];
    // We need to include computed fields, as we may have a URL alias.
    foreach ($entity->getFields(TRUE) as $field_name => $definition) {
      if ($this->straker_translateConfiguration->isFieldStrakerTranslateEnabled($entity->getEntityTypeId(), $entity->bundle(), $field_name)
        && $field_name != $entity_type->getKey('langcode')
        && $field_name != $entity_type->getKey('default_langcode')) {
        $translatable_fields[$field_name] = $definition;
      }
    }
    $default_display = $this->entityTypeManager->getStorage('entity_view_display')
      ->load($entity_type->id() . '.' . $entity->bundle() . '.' . 'default');
    if ($default_display !== NULL) {
      uksort($translatable_fields, function ($a, $b) use ($default_display) {
        return SortArray::sortByKeyString($default_display->getComponent($a), $default_display->getComponent($b), 'weight');
      });
    }

    $data = [];
    foreach ($translatable_fields as $field_name => $definition) {
      /** @var \Drupal\straker_translate\FieldProcessor\StrakerTranslateFieldProcessorManager $field_processor_manager */
      $field_processor_manager = \Drupal::service('plugin.manager.straker_translate_field_processor');
      /** @var \Drupal\straker_translate\FieldProcessor\StrakerTranslateFieldProcessorInterface[] $field_processors */
      $field_processors = $field_processor_manager->getProcessorsForField($field_definitions[$field_name], $source_entity);
      foreach ($field_processors as $field_processor) {
        $field_processor->extract($source_entity, $field_name, $field_definitions[$field_name], $data, $visited, $revision_mode);
      }
    }

    return $data;
  }

  /**
   * {@inheritdoc}
   */
  public function updateEntityHash(ContentEntityInterface $entity, $precalculated_source_data = []): void {
    $visited = [];
    $source_data = !empty($precalculated_source_data) ? Json::encode($precalculated_source_data) :
      Json::encode($this->getSourceData($entity, $visited, StrakerTranslateContentTranslationEntityRevisionResolver::RESOLVE_SAME));
    if ($entity->straker_translate_metadata->entity) {
      $entity->straker_translate_metadata->entity->hash = md5($source_data);
      $entity->straker_translate_metadata->entity->save();
    }
  }

  /**
   * {@inheritdoc}
   */
  public function hasEntityChanged(ContentEntityInterface &$entity) {
    if (isset($entity->original)) {
      $vid = $entity->getRevisionId();
      $org_vid = $entity->original->getRevisionId();
      if ($entity->getRevisionId() !== $entity->original->getRevisionId()) {
        // Return TRUE;.
      }
      $source_data = $this->getSourceData($entity);
      if (isset($source_data['_straker_translate_metadata'])) {
        unset($source_data['_straker_translate_metadata']['_entity_revision']);
      }
      $source_data = Json::encode($source_data);
      $hash = md5($source_data);
      $visited = [];
      $old_source_data = $this->getSourceData($entity->original, $visited, StrakerTranslateContentTranslationEntityRevisionResolver::RESOLVE_SAME);
      if (isset($old_source_data['_straker_translate_metadata'])) {
        unset($old_source_data['_straker_translate_metadata']['_entity_revision']);
      }
      $old_source_data = Json::encode($old_source_data);
      $old_hash = md5($old_source_data);
      return (bool) strcmp($hash, $old_hash);
    }
    else {
      return TRUE;
    }
  }

  /**
   * {@inheritdoc}
   */
  protected function setTranslatedTargetStatus(ContentEntityInterface &$entity, $response) {
    if (empty($response["data"]["source_files"][0]["target_files"])) {
      return;
    }
    $languageMapper = \Drupal::service('straker_translate.language_locale_mapper');
    foreach ($response["data"]["source_files"][0]["target_files"] as $target_file) {
      if ($target_file["status"] === 'translated') {
        $drupal_language = $languageMapper->getConfigurableLanguageForLocale($target_file["language_uuid"]);
        if ($drupal_language) {
          $this->setTargetStatus($entity, $drupal_language->id(), StrakerTranslate::STATUS_READY);
          // Set the file ID too.
          $target_file_id = $target_file['target_file_uuid'];
          $this->setTargetFileId($entity, $drupal_language->id(), $target_file_id);
        }
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function setTargetFileId(ContentEntityInterface &$entity, $langcode, $target_file_id) {
    $set = FALSE;
    if (!$entity->straker_translate_metadata->entity) {
      $entity->set('straker_translate_metadata', StrakerTranslateContentMetadata::loadByTargetId($entity->getEntityTypeId(), $entity->id()));
    }
    /** @var \Drupal\straker_translate\Entity\StrakerTranslateContentMetadata $metadata */
    $metadata = $entity->straker_translate_metadata ? $entity->straker_translate_metadata->entity : NULL;
    if ($metadata->hasField('translation_status') && count($metadata->translation_status) > 0) {
      /** @var \Drupal\Core\Entity\ContentEntityBase $translation_status */
      $translation_status = $metadata->get('translation_status');
      foreach ($translation_status->getIterator() as $delta => $value) {
        if ($value->language == $langcode) {
          $value->file_id = $target_file_id;
          $set = TRUE;
        }
      }
    }
    if ($set) {
      $metadata->save();
    }
  }

  /**
   * {@inheritdoc}
   *
   * @throws \Exception
   */
  public function getTargetFileId(ContentEntityInterface &$entity, $locale) {
    /** @var \Drupal\straker_translate\Entity\StrakerTranslateContentMetadata $metadata */
    $metadata = $entity->straker_translate_metadata ? $entity->straker_translate_metadata->entity : NULL;
    if ($metadata !== NULL && count($metadata->translation_status) > 0) {
      /** @var \Drupal\Core\Entity\ContentEntityBase $translation_status */
      $translation_status = $metadata->get('translation_status');
      foreach ($translation_status->getIterator() as $delta => $value) {
        if ($value->language == $locale) {
          // We return the document id for the requested locale.
          if (isset($value->file_id) && !empty($value->file_id)) {
            return $value->file_id;
          }
          return NULL;
        }
      }
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function uploadDocument(ContentEntityInterface $entity, $force_upload = FALSE) {
    $profile = $this->straker_translateConfiguration->getEntityProfile($entity);
    if (!$this->straker_translateConfiguration->isEnabled($entity->getEntityTypeId(), $entity->bundle())) {
      return FALSE;
    }
    if ($profile->id() === StrakerTranslate::PROFILE_DISABLED) {
      return FALSE;
    }

    $document_id = $this->getDocumentId($entity);
    if (!$force_upload) {
      if (in_array($this->getSourceStatus($entity), [StrakerTranslate::STATUS_CURRENT, StrakerTranslate::STATUS_READY, StrakerTranslate::STATUS_PROCESSING])) {
        throw new StrakerTranslateDocumentAlreadyUploaded('The document has already been uploaded and is being processed or is current.', 0);
        return FALSE;
      }
    }

    $source_data = $this->getSourceData($entity);
    if (empty($source_data)) {
      \Drupal::logger('straker_translate')->warning('No source data found for %entity:', ['%entity' => $entity->label()]);
      return FALSE;
    }
    $extended_name = $entity->bundle() . ' (' . $entity->getEntityTypeId() . '): ' . $entity->label();

    $profile_preference = $profile->getAppendContentTypeToTitle();
    $global_preference = $this->straker_translateConfiguration->getPreference('append_type_to_title');
    switch ($profile_preference) {
      case 'yes':
        $document_name = $extended_name;
        break;

      case 'no':
        $document_name = $entity->label();
        break;

      case 'global_setting':
        $document_name = $global_preference ? $extended_name : $entity->label();
        break;

      default:
        $document_name = $extended_name;
    }

    $filename = $entity->getEntityTypeId() . '_' . $entity->bundle() . '_' . $entity->id() . '.json';
    $token_confirmation = 'false';
    // Allow other modules to alter the data before is uploaded.
    \Drupal::moduleHandler()->invokeAll('straker_translate_content_entity_document_upload', [&$source_data, &$entity, &$url]);

    try {
      $entity_langcode = $entity->getUntranslated()->language()->getId();
      $target_languages = $this->languageManager->getLanguages();
      // Filter out the source language and disabled languages.
      $target_languages = array_filter($target_languages, function (LanguageInterface $language) use ($entity_langcode) {
        if ($language->getId() === $entity_langcode) {
          return FALSE;
        }
        $configLanguage = ConfigurableLanguage::load($language->getId());
        return $this->straker_translateConfiguration->isLanguageEnabled($configLanguage);
      });
      // Get langcodes only.
      if (empty($target_languages)) {
        // If there are no target languages, we can return.
        \Drupal::logger('straker_translate')->warning('No target languages configured for %entity_type_id:%bundle_id', ['%entity_type_id' => $entity->getEntityTypeId(), '%bundle_id' => $entity->bundle()]);
        return FALSE;
      }
      $target_langcodes = array_keys($target_languages);
      $target_locales = array_map([$this->languageLocaleMapper, 'getLocaleForLangcode'], $target_langcodes);
      $target_locale_uuid = implode(',', $target_locales);
      $project_id = $this->straker_translate->uploadDocument($document_name, $source_data, $target_locale_uuid, $token_confirmation, $filename, $profile);
    }
    catch (StrakerTranslatePaymentRequiredException $exception) {
      $this->setSourceStatus($entity, StrakerTranslate::STATUS_ERROR);
      throw $exception;
    }
    catch (StrakerTranslateProcessedWordsLimitException $exception) {
      $this->setSourceStatus($entity, StrakerTranslate::STATUS_ERROR);
      throw $exception;
    }
    catch (StrakerTranslateApiException $exception) {
      $this->setSourceStatus($entity, StrakerTranslate::STATUS_ERROR);
      throw $exception;
    }
    if ($project_id) {
      $this->setDocumentId($entity, $project_id);
      if ($token_confirmation === 'true') {
        $this->setSourceStatus($entity, StrakerTranslate::STATUS_IMPORTING);
      }
      else {
        $this->setSourceStatus($entity, StrakerTranslate::STATUS_READY);
      }
      $this->setTargetStatuses($entity, StrakerTranslate::STATUS_PENDING);
      $this->setLastUploaded($entity, \Drupal::time()->getRequestTime());
      $this->updateEntityHash($entity, $source_data);
      return $project_id;
    }
    if ($this->getSourceStatus($entity) == StrakerTranslate::STATUS_DISABLED) {
      $this->setTargetStatuses($entity, StrakerTranslate::STATUS_DISABLED);
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function confirmDocument(ContentEntityInterface &$entity) {
    $profile = $this->straker_translateConfiguration->getEntityProfile($entity);
    if ($profile->id() === StrakerTranslate::PROFILE_DISABLED || $this->getSourceStatus($entity) === StrakerTranslate::STATUS_CANCELLED) {
      return FALSE;
    }
    if ($this->getSourceStatus($entity) !== StrakerTranslate::STATUS_IMPORTING) {
      return FALSE;
    }
    $document_id = $this->getDocumentId($entity);
    try {
      $translation_status = $this->straker_translate->confirmDocument($document_id);
    }
    catch (StrakerTranslateDocumentAlreadyCompletedException $exception) {
      $this->setSourceStatus($entity, StrakerTranslate::STATUS_READY);
      throw new StrakerTranslateDocumentAlreadyCompletedException($exception->getMessage(), $exception->getCode());
    }
    catch (\Exception $e) {
      throw new StrakerTranslateDocumentAlreadyCompletedException($e->getMessage(), $e->getCode());
    }
    // If the document was confirmed, we can set the status to ready.
    if ($translation_status === StrakerTranslate::STATUS_READY) {
      $this->setSourceStatus($entity, StrakerTranslate::STATUS_READY);
      return TRUE;
    }
    else {
      $current_status = StrakerTranslate::STATUS_ERROR;
      $this->setSourceStatus($entity, StrakerTranslate::STATUS_ERROR);
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function downloadDocument(ContentEntityInterface &$entity, $file_id, $langcode) {
    $profile = $this->straker_translateConfiguration->getEntityProfile($entity);
    if ($profile->id() === StrakerTranslate::PROFILE_DISABLED || $this->getSourceStatus($entity) === StrakerTranslate::STATUS_CANCELLED) {
      \Drupal::logger('straker_translate')->warning('Avoided download for (%entity_id,%revision_id): Source status is %source_status.', ['%entity_id' => $entity->id(), '%revision_id' => $entity->getRevisionId(), '%source_status' => $this->getSourceStatus($entity)]);
      return FALSE;
    }
    $target_status = $this->getTargetStatus($entity, $langcode);
    if ($target_status === StrakerTranslate::STATUS_CURRENT) {
      \Drupal::logger('straker_translate')->info('Avoided download for (%entity_id,%revision_id): Target status is CURRENT.', ['%entity_id' => $entity->id()]);
      throw new StrakerTranslateDocumentAlreadyUploaded('The document has already been downloaded and processed or is current.', 0);
      return FALSE;
    }
    if ($document_id = $this->getDocumentId($entity)) {
      $data = [];
      $source_status = $this->getSourceStatus($entity);
      try {
        $data = $this->straker_translate->downloadDocument($file_id, $langcode);
      }
      catch (StrakerTranslateApiException $exception) {
        \Drupal::logger('straker_translate')->error('Error happened downloading %document_id %langcode: %message', ['%document_id' => $document_id, '%langcode' => $langcode, '%message' => $exception->getMessage()]);
        $this->setTargetStatus($entity, $langcode, StrakerTranslate::STATUS_ERROR);
        throw $exception;
      }

      if ($data) {
        // Check the real status, because it may still need review or anything.
        $transaction = $this->connection->startTransaction();
        try {
          $saved = $this->saveTargetData($entity, $langcode, $data);
          if ($saved) {
            // If the status was "Importing", and the target was added
            // successfully, we can ensure that the content is current now.
            if ($source_status == StrakerTranslate::STATUS_READY) {
              $this->setSourceStatus($entity, StrakerTranslate::STATUS_CURRENT);
            }
            if ($source_status == StrakerTranslate::STATUS_EDITED) {
              $this->setTargetStatus($entity, $langcode, StrakerTranslate::STATUS_EDITED);
            }
            $this->setTargetStatus($entity, $langcode, StrakerTranslate::STATUS_CURRENT);
          }
        }
        catch (StrakerTranslateContentEntityStorageException $storageException) {
          $this->setTargetStatus($entity, $langcode, StrakerTranslate::STATUS_ERROR);
          \Drupal::logger('straker_translate')->error('Error happened (storage) saving %document_id %langcode: %message', ['%document_id' => $document_id, '%langcode' => $langcode, '%message' => $storageException->getMessage()]);
          throw $storageException;
        }
        catch (\Exception $exception) {
          $transaction->rollBack();
          $this->setTargetStatus($entity, $langcode, StrakerTranslate::STATUS_ERROR);
          \Drupal::logger('straker_translate')->error('Error happened (unknown) saving %document_id %langcode: %message', ['%document_id' => $document_id, '%langcode' => $langcode, '%message' => $exception->getMessage()]);
          return FALSE;
        }
        return TRUE;
      }
    }
    if ($this->getSourceStatus($entity) == StrakerTranslate::STATUS_DISABLED) {
      $this->setTargetStatuses($entity, StrakerTranslate::STATUS_DISABLED);
    }
    \Drupal::logger('straker_translate')->warning('Error happened trying to download (%entity_id,%revision_id): no document id found.', ['%entity_id' => $entity->id(), '%revision_id' => $entity->getRevisionId()]);
    return FALSE;
  }

  /**
   *
   */
  public function downloadDocuments(ContentEntityInterface &$entity) {
    $profile = $this->straker_translateConfiguration->getEntityProfile($entity);
    if ($profile->id() === StrakerTranslate::PROFILE_DISABLED || $this->getSourceStatus($entity) === StrakerTranslate::STATUS_CANCELLED) {
      return FALSE;
    }
    if ($document_id = $this->getDocumentId($entity)) {
      $source_status = $this->getSourceStatus($entity);
      $target_languages = $this->languageManager->getLanguages();
      $target_languages = array_filter($target_languages, function (LanguageInterface $language) {
        $configLanguage = ConfigurableLanguage::load($language->getId());
        return $this->straker_translateConfiguration->isLanguageEnabled($configLanguage);
      });

      $entity_langcode = $entity->getUntranslated()->language()->getId();

      foreach ($target_languages as $langcode => $language) {
        if ($langcode !== $entity_langcode) {
          try {
            $file_id = $this->getTargetFileId($entity, $langcode);
            $data = $this->straker_translate->downloadDocument($file_id, $langcode);
            if ($data) {
              // Check the real status, because it may still need review or anything.
              $transaction = $this->connection->startTransaction();
              try {
                $saved = $this->saveTargetData($entity, $langcode, $data);
                if ($saved) {
                  if ($source_status == StrakerTranslate::STATUS_EDITED) {
                    $this->setTargetStatus($entity, $langcode, StrakerTranslate::STATUS_EDITED);
                  }
                  $this->setTargetStatus($entity, $langcode, StrakerTranslate::STATUS_CURRENT);
                }
              }
              catch (StrakerTranslateDocumentNotFoundException $exception) {
                throw $exception;
              }
              catch (StrakerTranslateApiException $exception) {
                // @todo log issue
                $this->setTargetStatus($entity, $langcode, StrakerTranslate::STATUS_ERROR);
                throw $exception;
              }
              catch (StrakerTranslateContentEntityStorageException $storageException) {
                $this->setTargetStatus($entity, $langcode, StrakerTranslate::STATUS_ERROR);
                throw $storageException;
              }
              catch (\Exception $exception) {
                $transaction->rollBack();
                $this->setTargetStatus($entity, $langcode, StrakerTranslate::STATUS_ERROR);
              }
            }
            else {
              return NULL;
            }
          }
          catch (StrakerTranslateDocumentNotFoundException $exception) {
            throw $exception;
          }
          catch (StrakerTranslateApiException $exception) {
            // @todo log issue
            $this->setTargetStatus($entity, $langcode, StrakerTranslate::STATUS_ERROR);
            throw $exception;
          }
        }
      }
    }
    if ($this->getSourceStatus($entity) == StrakerTranslate::STATUS_DISABLED) {
      $this->setTargetStatuses($entity, StrakerTranslate::STATUS_DISABLED);
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function deleteMetadata(ContentEntityInterface &$entity) {
    $doc_id = $this->getDocumentId($entity);
    if ($doc_id) {
      // $this->cancelDocument($entity);
    }
    /** @var \Drupal\straker_translate\Entity\StrakerTranslateContentMetadata $metadata */
    $metadata = $entity->straker_translate_metadata->entity;
    if ($metadata !== NULL) {
      $metadata->delete();
    }
    return $entity;
  }

  /**
   * {@inheritdoc}
   */
  public function loadByDocumentId($document_id) {
    $entity = NULL;
    $metadata = StrakerTranslateContentMetadata::loadByDocumentID($document_id);
    if ($metadata && $metadata->getContentEntityTypeId() && $metadata->getContentEntityId()) {
      $entity = $this->entityTypeManager->getStorage($metadata->getContentEntityTypeId())->load($metadata->getContentEntityId());
    }
    return $entity;
  }

  /**
   * {@inheritdoc}
   */
  public function getAllLocalDocumentIds() {
    return StrakerTranslateContentMetadata::getAllLocalDocumentIds();
  }

  /**
   * Loads the correct revision is loaded from the database, bypassing caches.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity we want to load a revision from.
   * @param int|null $revision
   *   The revision id. NULL if we don't know it.
   *
   * @return \Drupal\Core\Entity\ContentEntityInterface
   *   The wanted revision of the entity.
   */
  protected function loadUploadedRevision(ContentEntityInterface $entity, $revision = NULL) {
    $the_revision = NULL;

    $entity_type = $entity->getEntityType();
    $entity_type_id = $entity->getEntityTypeId();
    $entity_storage = $this->entityTypeManager->getStorage($entity_type_id);

    if ($entity_type->isRevisionable()) {
      // If the entity type is revisionable, we need to check the proper revision.
      // This may come from the uploaded data, but in case we didn't have it, we
      // have to infer using the timestamp.
      if ($revision !== NULL) {
        /** @var \Drupal\Core\Entity\RevisionableStorageInterface $entity_storage */
        $the_revision = $entity_storage->loadRevision($revision);
      }
      elseif ($revision === NULL && $entity->hasField('revision_timestamp')) {
        // Let's find the better revision based on the timestamp.
        $timestamp = $this->straker_translate->getUploadedTimestamp($this->getDocumentId($entity));
        $revision = $this->getClosestRevisionToTimestamp($entity, $timestamp);
        if ($revision !== NULL) {
          /** @var \Drupal\Core\Entity\RevisionableStorageInterface $entity_storage */
          $the_revision = $entity_storage->loadRevision($revision);
        }
      }
      if ($the_revision === NULL) {
        // We didn't find a better option, but let's reload this one so it's not
        // cached.
        /** @var \Drupal\Core\Entity\RevisionableStorageInterface $entity_storage */
        $the_revision = $entity_storage->loadRevision($entity->getRevisionId());
      }
    }
    else {
      $entity_storage->resetCache([$entity->id()]);
      $the_revision = $entity_storage->load($entity->id());
    }
    return $the_revision;
  }

  /**
   *
   */
  protected function getClosestRevisionToTimestamp(ContentEntityInterface &$entity, $timestamp) {
    $entity_id = $entity->id();

    $query = \Drupal::database()->select($entity->getEntityType()->getRevisionDataTable(), 'nfr');
    $query->fields('nfr', [$entity->getEntityType()->getKey('revision')]);
    $query->addJoin('INNER', $entity->getEntityType()->getRevisionTable(), 'nr',
        'nfr.vid = nr.vid and nfr.nid = nr.nid and nfr.langcode = nr.langcode'
      );
    $query->condition('nfr.' . $entity->getEntityType()->getKey('id'), $entity_id);
    $query->condition('nfr.' . $entity->getEntityType()->getKey('langcode'), $entity->language()->getId());
    $query->condition('nr.revision_timestamp', $timestamp, '<');
    $query->orderBy('nfr.changed', 'DESC');
    $query->range(0, 1);

    $value = $query->execute();
    $vids = $value->fetchAssoc();
    return ($vids !== FALSE && count($vids) === 1) ? $vids['vid'] : NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function saveTargetData(ContentEntityInterface &$entity, $langcode, $data) {
    // Without a defined langcode, we can't proceed.
    if (!$langcode) {
      // @todo log warning that downloaded translation's langcode is not enabled.
      return FALSE;
    }
    $storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity->getEntityTypeId());

    try {
      // We need to load the revision that was uploaded for consistency. For that,
      // we check if we have a valid revision in the response, and if not, we
      // check the date of the uploaded document.
      $entity_type = $entity->getEntityTypeId();
      $entity_id = $entity->id();

      $storage = \Drupal::entityTypeManager()->getStorage($entity_type);

      // Get latest revision ID for this entity.
      $latest_revision_id = $storage->getLatestRevisionId($entity_id);
      $revision = $storage->loadRevision($latest_revision_id);

      // We should reload the last revision of the entity at all times.
      // This check here is only because of the case when we have asymmetric
      // paragraphs for translations, as in that case we get a duplicate that
      // still has not a valid entity id.
      // Also take into account that we may have just removed paragraph
      // translations form previous translation approaches, and in that case we
      // are forced to remove those, but there will be a mark of translation
      // changes.
      if ($entity->id() && !$entity->hasTranslationChanges()) {
        $entity = $this->entityTypeManager->getStorage($entity->getEntityTypeId())->load($entity->id());
      }

      // Initialize the translation on the Drupal side, if necessary.
      /** @var \Drupal\Core\Entity\ContentEntityBase $entity */
      if (!$entity->hasTranslation($langcode)) {
        $entity->addTranslation($langcode, $revision->toArray());
      }

      $translation = $entity->getTranslation($langcode);
      foreach ($data as $field_name => $field_data) {
        if (strpos($field_name, '_') === 0) {
          // Skip special fields underscored.
          break;
        }
        $field_definition = $entity->getFieldDefinition($field_name);
        if ($field_definition && ($field_definition->isTranslatable() || $field_definition->getType() === 'cohesion_entity_reference_revisions' || $field_definition->getType() === 'entity_reference_revisions')
          && $this->straker_translateConfiguration->isFieldStrakerTranslateEnabled($entity->getEntityTypeId(), $entity->bundle(), $field_name)) {

          /** @var \Drupal\straker_translate\FieldProcessor\StrakerTranslateFieldProcessorManager $field_processor_manager */
          $field_processor_manager = \Drupal::service('plugin.manager.straker_translate_field_processor');
          /** @var \Drupal\straker_translate\FieldProcessor\StrakerTranslateFieldProcessorInterface[] $field_processors */
          $field_processors = $field_processor_manager->getProcessorsForField($field_definition, $revision);
          // For persisting, only one processor can apply for avoiding conflicts.
          if (count($field_processors) > 0) {
            $field_processors = array_reverse($field_processors);
            $field_processor = reset($field_processors);
            $field_processor->store($translation, $langcode, $revision, $field_name, $field_definition, $data[$field_name]);
          }
          else {
            \Drupal::logger('straker_translate')->error('Error persisting %entity_type_id %entity_id (%label) translation to %langcode because there were no processors for field %field_name', [
              '%entity_type_id' => $revision->getEntityTypeId(),
              '%entity_id' => $revision->id(),
              '%label' => $revision->label(),
              '%langcode' => $langcode,
              '%field_name' => $field_name,
            ]);
          }
        }
      }

      // We need to set the content_translation source so the files are synced
      // properly. See https://www.drupal.org/node/2544696 for more information.
      $translation->set('content_translation_source', $entity->getUntranslated()->language()->getId());

      $entity->straker_translate_processed = TRUE;
      // Allow other modules to alter the translation before is saved.
      \Drupal::moduleHandler()->invokeAll('straker_translate_content_entity_translation_presave', [&$translation, $langcode, $data]);

      $published_field = $entity->getEntityType()->getKey('published');
      $published_field_definition = $entity->getFieldDefinition($published_field);
      if ($published_field_definition !== NULL && $published_field_definition->isTranslatable()) {
        $published_setting = $this->straker_translateConfiguration->getPreference('target_download_status');
        if ($published_setting !== "same-as-source") {
          $published_value = ($published_setting === 'published') ? NodeInterface::PUBLISHED : NodeInterface::NOT_PUBLISHED;
          $translation->set($published_field, $published_value);
        }
        if ($entity->getEntityTypeId() === 'paragraph') {
          $translation->set($published_field, $revision->get($revision->getEntityType()->getKey('published'))->value);
        }
      }

      // If there is any content moderation module is enabled, we may need to
      // perform a transition in their workflow.
      /** @var \Drupal\straker_translate\Moderation\StrakerTranslateModerationFactoryInterface $moderation_factory */
      $moderation_factory = \Drupal::service('straker_translate.moderation_factory');
      $moderation_handler = $moderation_factory->getModerationHandler();
      $moderation_handler->performModerationTransitionIfNeeded($translation);

      if ($moderation_handler->isModerationEnabled($translation) &&
          $translation->getEntityType()->isRevisionable()) {
        if ($bundle_entity_type = $entity->getEntityType()->getBundleEntityType()) {
          $bundle_entity = $this->entityTypeManager->getStorage($bundle_entity_type)->load($entity->bundle());
          if ($bundle_entity instanceof RevisionableEntityBundleInterface) {
            $translation->setNewRevision($bundle_entity->shouldCreateNewRevision());
          }
        }
        if ($translation instanceof RevisionLogInterface && $translation->isNewRevision()) {
          $requestTime = \Drupal::time()->getRequestTime();
          $translation->setRevisionUserId(\Drupal::currentUser()->id());
          $translation->setRevisionCreationTime($requestTime);
          $translation->setRevisionLogMessage((string) new FormattableMarkup('Document translated into @langcode by Straker Translate.', ['@langcode' => strtoupper($langcode)]));
        }
      }
      $translation->save();

      return $entity;
    }
    catch (EntityStorageException $storage_exception) {
      $this->setTargetStatus($entity, $langcode, StrakerTranslate::STATUS_ERROR);
      throw new StrakerTranslateContentEntityStorageException($entity, $storage_exception, $storage_exception->getMessage());
    }
  }

  /**
   * {@inheritdoc}
   */
  public function setLastUploaded(ContentEntityInterface $entity, int $timestamp) {
    if (!$entity->straker_translate_metadata) {
      $entity->set('straker_translate_metadata', StrakerTranslateContentMetadata::loadByTargetId($entity->getEntityTypeId(), $entity->id()));
    }
    $entity_object = $entity->straker_translate_metadata->entity;
    /** @var \Drupal\straker_translate\Entity\StrakerTranslateContentMetadata $metadata */
    $metadata = &$entity_object;
    $metadata->setLastUploaded($timestamp)->save();

    return $entity;
  }

  /**
   * {@inheritdoc}
   */
  public function setLastUpdated(ContentEntityInterface $entity, int $timestamp) {
    if (!$entity->straker_translate_metadata) {
      $entity->set('straker_translate_metadata', StrakerTranslateContentMetadata::loadByTargetId($entity->getEntityTypeId(), $entity->id()));
    }
    $entity_object = $entity->straker_translate_metadata->entity;
    /** @var \Drupal\straker_translate\Entity\StrakerTranslateContentMetadata $metadata */
    $metadata = &$entity_object;
    $metadata->setLastUpdated($timestamp)->save();

    return $entity;
  }

  /**
   * {@inheritdoc}
   */
  public function getLastUploaded(ContentEntityInterface $entity) {
    if (!$entity->straker_translate_metadata) {
      $entity->set('straker_translate_metadata', StrakerTranslateContentMetadata::loadByTargetId($entity->getEntityTypeId(), $entity->id()));
    }
    /** @var \Drupal\straker_translate\Entity\StrakerTranslateContentMetadata $metadata */
    $metadata = $entity->straker_translate_metadata->entity;
    return $metadata->getLastUploaded();
  }

  /**
   * {@inheritdoc}
   */
  public function getLastUpdated(ContentEntityInterface $entity) {
    if (!$entity->straker_translate_metadata) {
      $entity->set('straker_translate_metadata', StrakerTranslateContentMetadata::loadByTargetId($entity->getEntityTypeId(), $entity->id()));
    }
    /** @var \Drupal\straker_translate\Entity\StrakerTranslateContentMetadata $metadata */
    $metadata = $entity->straker_translate_metadata->entity;
    return $metadata->getLastUpdated();
  }

}
