/**
 * Orthanc - A Lightweight, RESTful DICOM Store
 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 * Department, University Hospital of Liege, Belgium
 * Copyright (C) 2017-2023 Osimis S.A., Belgium
 * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium
 * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 **/


#include "../../PrecompiledHeadersServer.h"
#include "DatabaseLookup.h"

#include "../../../../OrthancFramework/Sources/OrthancException.h"
#include "../../Search/DicomTagConstraint.h"
#include "../../ServerToolbox.h"
#include "SetOfResources.h"

namespace Orthanc
{
  namespace Compatibility
  {
    namespace
    {
      // Anonymous namespace to avoid clashes between compiler modules
      class MainTagsConstraints : boost::noncopyable
      {
      private:
        std::vector<DicomTagConstraint*>  constraints_;

      public:
        ~MainTagsConstraints()
        {
          for (size_t i = 0; i < constraints_.size(); i++)
          {
            assert(constraints_[i] != NULL);
            delete constraints_[i];
          }
        }

        void Reserve(size_t n)
        {
          constraints_.reserve(n);
        }

        size_t GetSize() const
        {
          return constraints_.size();
        }

        DicomTagConstraint& GetConstraint(size_t i) const
        {
          if (i >= constraints_.size())
          {
            throw OrthancException(ErrorCode_ParameterOutOfRange);
          }
          else
          {
            assert(constraints_[i] != NULL);
            return *constraints_[i];
          }
        }
        
        void Add(const DatabaseDicomTagConstraint& constraint)
        {
          constraints_.push_back(new DicomTagConstraint(constraint));
        }          
      };
    }
    
    
    static void ApplyIdentifierConstraint(SetOfResources& candidates,
                                          ILookupResources& compatibility,
                                          const DatabaseDicomTagConstraint& constraint,
                                          ResourceType level)
    {
      std::list<int64_t> matches;

      switch (constraint.GetConstraintType())
      {
        case ConstraintType_Equal:
          compatibility.LookupIdentifier(matches, level, constraint.GetTag(),
                                    IdentifierConstraintType_Equal, constraint.GetSingleValue());
          break;
          
        case ConstraintType_SmallerOrEqual:
          compatibility.LookupIdentifier(matches, level, constraint.GetTag(),
                                    IdentifierConstraintType_SmallerOrEqual, constraint.GetSingleValue());
          break;
          
        case ConstraintType_GreaterOrEqual:
          compatibility.LookupIdentifier(matches, level, constraint.GetTag(),
                                    IdentifierConstraintType_GreaterOrEqual, constraint.GetSingleValue());

          break;
          
        case ConstraintType_Wildcard:
          compatibility.LookupIdentifier(matches, level, constraint.GetTag(),
                                    IdentifierConstraintType_Wildcard, constraint.GetSingleValue());

          break;
          
        case ConstraintType_List:
          for (size_t i = 0; i < constraint.GetValuesCount(); i++)
          {
            std::list<int64_t> tmp;
            compatibility.LookupIdentifier(tmp, level, constraint.GetTag(),
                                      IdentifierConstraintType_Wildcard, constraint.GetValue(i));
            matches.splice(matches.end(), tmp);
          }

          break;
          
        default:
          THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_InternalError);
      }

      candidates.Intersect(matches);
    }

    
    static void ApplyIdentifierRange(SetOfResources& candidates,
                                     ILookupResources& compatibility,
                                     const DatabaseDicomTagConstraint& smaller,
                                     const DatabaseDicomTagConstraint& greater,
                                     ResourceType level)
    {
      assert(smaller.GetConstraintType() == ConstraintType_SmallerOrEqual &&
             greater.GetConstraintType() == ConstraintType_GreaterOrEqual &&
             smaller.GetTag() == greater.GetTag() &&
             ServerToolbox::IsIdentifier(smaller.GetTag(), level));

      std::list<int64_t> matches;
      compatibility.LookupIdentifierRange(matches, level, smaller.GetTag(),
                                     greater.GetSingleValue(), smaller.GetSingleValue());
      candidates.Intersect(matches);
    }

    
    static void ApplyLevel(SetOfResources& candidates,
                           IDatabaseWrapper::ITransaction& transaction,
                           ILookupResources& compatibility,
                           const DatabaseDicomTagConstraints& lookup,
                           ResourceType level)
    {
      typedef std::set<const DatabaseDicomTagConstraint*>  SetOfConstraints;
      typedef std::map<DicomTag, SetOfConstraints> Identifiers;

      // (1) Select which constraints apply to this level, and split
      // them between "identifier tags" constraints and "main DICOM
      // tags" constraints

      Identifiers       identifiers;
      SetOfConstraints  mainTags;
      
      for (size_t i = 0; i < lookup.GetSize(); i++)
      {
        const DatabaseDicomTagConstraint& constraint = lookup.GetConstraint(i);

        if (constraint.GetLevel() == level)
        {
          if (constraint.IsIdentifier())
          {
            identifiers[constraint.GetTag()].insert(&constraint);
          }
          else
          {
            mainTags.insert(&constraint);
          }
        }
      }

      
      // (2) Apply the constraints over the identifiers
      
      for (Identifiers::const_iterator it = identifiers.begin();
           it != identifiers.end(); ++it)
      {
        // Check whether some range constraint over identifiers is
        // present at this level
        const DatabaseDicomTagConstraint* smaller = NULL;
        const DatabaseDicomTagConstraint* greater = NULL;
        
        for (SetOfConstraints::const_iterator it2 = it->second.begin();
             it2 != it->second.end(); ++it2)
        {
          assert(*it2 != NULL);
        
          if ((*it2)->GetConstraintType() == ConstraintType_SmallerOrEqual)
          {
            smaller = *it2;
          }

          if ((*it2)->GetConstraintType() == ConstraintType_GreaterOrEqual)
          {
            greater = *it2;
          }
        }

        if (smaller != NULL &&
            greater != NULL)
        {
          // There is a range constraint: Apply it, as it is more efficient
          ApplyIdentifierRange(candidates, compatibility, *smaller, *greater, level);
        }
        else
        {
          smaller = NULL;
          greater = NULL;
        }

        for (SetOfConstraints::const_iterator it2 = it->second.begin();
             it2 != it->second.end(); ++it2)
        {
          // Check to avoid applying twice the range constraint
          if (*it2 != smaller &&
              *it2 != greater)
          {
            ApplyIdentifierConstraint(candidates, compatibility, **it2, level);
          }
        }
      }


      // (3) Apply the constraints over the main DICOM tags (no index
      // here, so this is less efficient than filtering over the
      // identifiers)
      if (!mainTags.empty())
      {
        MainTagsConstraints c;
        c.Reserve(mainTags.size());
        
        for (SetOfConstraints::const_iterator it = mainTags.begin();
             it != mainTags.end(); ++it)
        {
          assert(*it != NULL);
          c.Add(**it);
        }

        std::list<int64_t>  source;
        candidates.Flatten(compatibility, source);
        candidates.Clear();

        std::list<int64_t>  filtered;
        for (std::list<int64_t>::const_iterator candidate = source.begin(); 
             candidate != source.end(); ++candidate)
        {
          DicomMap tags;
          transaction.GetMainDicomTags(tags, *candidate);

          bool match = true;

          for (size_t i = 0; i < c.GetSize(); i++)
          {
            if (!c.GetConstraint(i).IsMatch(tags))
            {
              match = false;
              break;
            }
          }
        
          if (match)
          {
            filtered.push_back(*candidate);
          }
        }

        candidates.Intersect(filtered);
      }
    }


    static std::string GetOneInstance(IDatabaseWrapper::ITransaction& compatibility,
                                      int64_t resource,
                                      ResourceType level)
    {
      for (int i = level; i < ResourceType_Instance; i++)
      {
        assert(compatibility.GetResourceType(resource) == static_cast<ResourceType>(i));

        std::list<int64_t> children;
        compatibility.GetChildrenInternalId(children, resource);
          
        if (children.empty())
        {
          throw OrthancException(ErrorCode_Database);
        }
          
        resource = children.front();
      }

      return compatibility.GetPublicId(resource);
    }
                           

    void DatabaseLookup::ApplyLookupResources(std::list<std::string>& resourcesId,
                                              std::list<std::string>* instancesId,
                                              const DatabaseDicomTagConstraints& lookup,
                                              ResourceType queryLevel,
                                              size_t limit)
    {
      // This is a re-implementation of
      // "../../../Resources/Graveyard/DatabaseOptimizations/LookupResource.cpp"

      assert(ResourceType_Patient < ResourceType_Study &&
             ResourceType_Study < ResourceType_Series &&
             ResourceType_Series < ResourceType_Instance);
    
      ResourceType upperLevel = queryLevel;
      ResourceType lowerLevel = queryLevel;

      for (size_t i = 0; i < lookup.GetSize(); i++)
      {
        ResourceType level = lookup.GetConstraint(i).GetLevel();

        if (level < upperLevel)
        {
          upperLevel = level;
        }

        if (level > lowerLevel)
        {
          lowerLevel = level;
        }
      }

      assert(upperLevel <= queryLevel &&
             queryLevel <= lowerLevel);

      SetOfResources candidates(transaction_, upperLevel);

      for (int level = upperLevel; level <= lowerLevel; level++)
      {
        ApplyLevel(candidates, transaction_, compatibility_, lookup, static_cast<ResourceType>(level));

        if (level != lowerLevel)
        {
          candidates.GoDown();
        }
      }

      std::list<int64_t> resources;
      candidates.Flatten(compatibility_, resources);

      // Climb up, up to queryLevel

      for (int level = lowerLevel; level > queryLevel; level--)
      {
        std::list<int64_t> parents;
        for (std::list<int64_t>::const_iterator
               it = resources.begin(); it != resources.end(); ++it)
        {
          int64_t parent;
          if (transaction_.LookupParent(parent, *it))
          {
            parents.push_back(parent);
          }
        }

        resources.swap(parents);
      }

      // Apply the limit, if given

      if (limit != 0 &&
          resources.size() > limit)
      {
        resources.resize(limit);
      }

      // Get the public ID of all the selected resources

      size_t pos = 0;

      for (std::list<int64_t>::const_iterator
             it = resources.begin(); it != resources.end(); ++it, pos++)
      {
        assert(transaction_.GetResourceType(*it) == queryLevel);

        const std::string resource = transaction_.GetPublicId(*it);
        resourcesId.push_back(resource);

        if (instancesId != NULL)
        {
          if (queryLevel == ResourceType_Instance)
          {
            // The resource is itself the instance
            instancesId->push_back(resource);
          }
          else
          {
            // Collect one child instance for each of the selected resources
            instancesId->push_back(GetOneInstance(transaction_, *it, queryLevel));
          }
        }
      }
    }
  }
}
