/*=========================================================================

  Library:   CTK

  Copyright (c) PerkLab 2013

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0.txt

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.

=========================================================================*/

#ifndef __ctkDICOMDisplayedFieldGeneratorAbstractRule_h
#define __ctkDICOMDisplayedFieldGeneratorAbstractRule_h

// Qt includes
#include <QMap>
#include <QMultiMap>
#include <QStringList>

// DCMTK includes
#include <dcmtk/dcmdata/dcdeftag.h>

#include "ctkDICOMCoreExport.h"

class ctkDICOMDatabase;

/// \ingroup DICOM_Core
///
/// Abstract base class for generating displayed fields from DICOM fields.
///
/// The displayed field generator classes need to be registered in \sa ctkDICOMDisplayedFieldGenerator
/// so that the rules it defines are taken into account when generating the displayed fields.
///
class CTK_DICOM_CORE_EXPORT ctkDICOMDisplayedFieldGeneratorAbstractRule
{
public:
  virtual ~ctkDICOMDisplayedFieldGeneratorAbstractRule(){}

  /// Get name of rule
  virtual QString name()const = 0;

  /// Clone displayed field generator rule. Override to return a new instance of the rule sub-class
  virtual ctkDICOMDisplayedFieldGeneratorAbstractRule* clone() = 0;

  /// Generate displayed fields for a certain instance based on its cached tags
  /// Each rule plugin has the chance to fill any field in the series, study, and patient fields.
  /// The way these generated fields will be used is defined by \sa mergeDisplayedFieldsForInstance
  virtual void getDisplayedFieldsForInstance(
    const QMap<QString, QString> &cachedTagsForInstance,
    QMap<QString, QString> &displayedFieldsForCurrentSeries,
    QMap<QString, QString> &displayedFieldsForCurrentStudy,
    QMap<QString, QString> &displayedFieldsForCurrentPatient)
  {
    Q_UNUSED(cachedTagsForInstance);
    Q_UNUSED(displayedFieldsForCurrentSeries);
    Q_UNUSED(displayedFieldsForCurrentStudy);
    Q_UNUSED(displayedFieldsForCurrentPatient);
  }

  /// Define rules of merging initial database values with new values generated by the rule plugins
  /// Currently available options:
  /// - \sa mergeExpectSameValue Use the non-empty field value. If both initial and new are non-empty and different, then use the initial value
  /// - \sa mergeConcatenate Use the non-empty field value. If both initial and new are non-empty, then concatenate them using comma
  virtual void mergeDisplayedFieldsForInstance(
    const QMap<QString, QString> &initialFieldsSeries,
    const QMap<QString, QString> &initialFieldsStudy,
    const QMap<QString, QString> &initialFieldsPatient,
    const QMap<QString, QString> &newFieldsSeries,
    const QMap<QString, QString> &newFieldsStudy,
    const QMap<QString, QString> &newFieldsPatient,
    QMap<QString, QString> &mergedFieldsSeries,
    QMap<QString, QString> &mergedFieldsStudy,
    QMap<QString, QString> &mergedFieldsPatient,
    const QMultiMap<QString, QString> &emptyFieldsSeries,
    const QMultiMap<QString, QString> &emptyFieldsStudy,
    const QMultiMap<QString, QString> &emptyFieldsPatient)
  {
    Q_UNUSED(initialFieldsSeries);
    Q_UNUSED(initialFieldsStudy);
    Q_UNUSED(initialFieldsPatient);
    Q_UNUSED(newFieldsSeries);
    Q_UNUSED(newFieldsStudy);
    Q_UNUSED(newFieldsPatient);
    Q_UNUSED(mergedFieldsSeries);
    Q_UNUSED(mergedFieldsStudy);
    Q_UNUSED(mergedFieldsPatient);
    Q_UNUSED(emptyFieldsSeries);
    Q_UNUSED(emptyFieldsStudy);
    Q_UNUSED(emptyFieldsPatient);
  }

  /// Specify list of DICOM tags required by the rule. These tags will be included in the tag cache
  virtual QStringList getRequiredDICOMTags()=0;

  /// Start updating displayed fields (reset counters, etc.). No-op by default.
  virtual void startUpdate() { }

  /// End updating displayed fields (accumulate stored variables, compute final result, etc.). No-op by default.
  /// Has a chance to update any field in the series, study, or patient field maps, based on
  /// the maps themselves or the database.
  virtual void endUpdate(QMap<QString, QMap<QString, QString> > &displayedFieldsMapSeries,
                         QMap<QString, QMap<QString, QString> > &displayedFieldsMapStudy,
                         QMap<QString, QMap<QString, QString> > &displayedFieldsMapPatient)
  {
    Q_UNUSED(displayedFieldsMapSeries);
    Q_UNUSED(displayedFieldsMapStudy);
    Q_UNUSED(displayedFieldsMapPatient);
  }

  /// Utility function to convert a DICOM tag enum to string
  static QString dicomTagToString(const DcmTagKey& tag)
  {
    return QString("%1,%2").arg(tag.getGroup(),4,16,QLatin1Char('0')).arg(tag.getElement(),4,16,QLatin1Char('0')).toUpper();
  }

  /// Register placeholder strings that still mean that a given field can be considered empty.
  /// Used when merging the original database content with the displayed fields generated by the rules.
  /// Example: SeriesDescription -> Unnamed Series
  virtual void registerEmptyFieldNames(
    QMultiMap<QString, QString> emptyFieldsSeries,
    QMultiMap<QString, QString> emptyFieldsStudies,
    QMultiMap<QString, QString> emptyFieldsPatients)
  {
    Q_UNUSED(emptyFieldsSeries);
    Q_UNUSED(emptyFieldsStudies);
    Q_UNUSED(emptyFieldsPatients);
  }

  /// Set DICOM database to the rule in case it needs to use it e.g. in \sa end().
  void setDatabase(ctkDICOMDatabase* database) { this->DICOMDatabase = database; }

// Static utility functions
public:
  /// Utility function determining whether a given field is considered empty
  static bool isFieldEmpty(const QString &fieldName, const QMap<QString, QString> &fields, const QMultiMap<QString, QString> &emptyValuesForEachField)
  {
    if (!fields.contains(fieldName))
    {
      // the field is not present
      return true;
    }
    if (fields[fieldName].isEmpty())
    {
      // the field is present, but empty
      return true;
    }
    if (emptyValuesForEachField.values(fieldName).contains(fields[fieldName]))
    {
      // the field is not empty, but contain a placeholder string (example: "No description") that means that the field is undefined
      return true;
    }
    // this field is non-empty
    return false;
  }

  /// Merge function that only uses the new value if the initial value is empty and vice versa
  static void mergeExpectSameValue(
    const QString &fieldName,
    const QMap<QString, QString> &initialFields,
    const QMap<QString, QString> &newFields,
    QMap<QString, QString> &mergedFields,
    const QMultiMap<QString, QString> &emptyValuesForEachField)
  {
    if (isFieldEmpty(fieldName, newFields, emptyValuesForEachField))
    {
      // no new value is defined for this value, keep the initial value (if exists)
      if (!isFieldEmpty(fieldName, initialFields, emptyValuesForEachField))
      {
        mergedFields[fieldName]=initialFields[fieldName];
      }
      return;
    }
    if (isFieldEmpty(fieldName, initialFields, emptyValuesForEachField))
    {
      // no initial value is defined for this value, use just the new value (if exists)
      if (!isFieldEmpty(fieldName, newFields, emptyValuesForEachField))
      {
        mergedFields[fieldName]=newFields[fieldName];
      }
      return;
    }
    // both initial and new value are defined and they are different => just keep using the old value
    // TODO: log warning here, as this is not expected
    mergedFields[fieldName]=initialFields[fieldName];
  }

  /// Merge function that sets merged value as a concatenation of the initial and new values
  /// Individual values in the concatenation are separated by comma
  static void mergeConcatenate(
    const QString &fieldName,
    const QMap<QString, QString> &initialFields,
    const QMap<QString, QString> &newFields,
    QMap<QString, QString> &mergedFields,
    const QMultiMap<QString, QString> &emptyValuesForEachField)
  {
    if (isFieldEmpty(fieldName, newFields, emptyValuesForEachField))
    {
      // no new value is defined for this value, keep the initial value (if exists)
      if (!isFieldEmpty(fieldName, initialFields, emptyValuesForEachField))
      {
        mergedFields[fieldName]=initialFields[fieldName];
      }
      return;
    }
    if (isFieldEmpty(fieldName, initialFields, emptyValuesForEachField))
    {
      // no initial value is defined for this value, use just the new value (if exists)
      if (!isFieldEmpty(fieldName, newFields, emptyValuesForEachField))
      {
        mergedFields[fieldName]=newFields[fieldName];
      }
      return;
    }
    if (initialFields[fieldName].contains(newFields[fieldName]))
    {
      // the field is already contained in the list, so no need to add it
      mergedFields[fieldName]=initialFields[fieldName];
      return;
    }
    // need to concatenate the new value to the initial
    mergedFields[fieldName]=initialFields[fieldName]+", "+newFields[fieldName];
  }

protected:
  ctkDICOMDatabase* DICOMDatabase;
};

#endif
