/**
 * 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 Lesser 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program. If not, see
 * <http://www.gnu.org/licenses/>.
 **/


#include "../PrecompiledHeaders.h"
#include "IWebDavBucket.h"

#include "HttpOutput.h"
#include "../OrthancException.h"
#include "../Toolbox.h"


static boost::posix_time::ptime GetNow()
{
  return boost::posix_time::second_clock::universal_time();
}


static std::string AddTrailingSlash(const std::string& s)
{
  if (s.empty() ||
      s[s.size() - 1] != '/')
  {
    return s + '/';
  }
  else
  {
    return s;
  }
}

static std::string AddLeadingSlash(const std::string& s)
{
  if (s.empty() ||
      s[0] != '/')
  {
    return std::string("/") + s;
  }
  else
  {
    return s;
  }
}


namespace Orthanc
{
  IWebDavBucket::Resource::Resource(const std::string& displayName) :
    displayName_(displayName),
    hasModificationTime_(false),
    creationTime_(GetNow()),
    modificationTime_(GetNow())
  {
    if (displayName.empty() ||
        displayName.find('/') != std::string::npos ||
        displayName.find('\\') != std::string::npos ||
        displayName.find('\0') != std::string::npos)
    {
      throw OrthancException(ErrorCode_ParameterOutOfRange,
                             "Bad resource name for WebDAV: " + displayName);
    }
  }


  void IWebDavBucket::Resource::SetCreationTime(const boost::posix_time::ptime& t)
  {
    if (t.is_special())
    {
      throw OrthancException(ErrorCode_ParameterOutOfRange, "Not a valid date-time");
    }
    else
    {
      creationTime_ = t;
      
      if (!hasModificationTime_)
      {
        modificationTime_ = t;
      }
    }
  }


  void IWebDavBucket::Resource::SetModificationTime(const boost::posix_time::ptime& t)
  {
    if (t.is_special())
    {
      throw OrthancException(ErrorCode_ParameterOutOfRange, "Not a valid date-time");
    }
    else
    {
      modificationTime_ = t;
      hasModificationTime_ = true;
    }
  }


  static void FormatInternal(pugi::xml_node& node,
                             const std::string& href,
                             const std::string& displayName,
                             const boost::posix_time::ptime& creationTime,
                             const boost::posix_time::ptime& modificationTime)
  {
    node.set_name("D:response");

    node.append_child("D:href").append_child(pugi::node_pcdata).set_value(href.c_str());

    pugi::xml_node propstat = node.append_child("D:propstat");

    static const HttpStatus status = HttpStatus_200_Ok;
    std::string s = ("HTTP/1.1 " + boost::lexical_cast<std::string>(status) + " " +
                     std::string(EnumerationToString(status)));
    propstat.append_child("D:status").append_child(pugi::node_pcdata).set_value(s.c_str());

    pugi::xml_node prop = propstat.append_child("D:prop");
    prop.append_child("D:displayname").append_child(pugi::node_pcdata).set_value(displayName.c_str());

    // IMPORTANT: Adding the "Z" suffix is mandatory on Windows >= 7 (it indicates UTC)
    assert(!creationTime.is_special());
    s = boost::posix_time::to_iso_extended_string(creationTime) + "Z";
    prop.append_child("D:creationdate").append_child(pugi::node_pcdata).set_value(s.c_str());

    assert(!modificationTime.is_special());
    s = boost::posix_time::to_iso_extended_string(modificationTime) + "Z";
    prop.append_child("D:getlastmodified").append_child(pugi::node_pcdata).set_value(s.c_str());

#if 0
    // Maybe used by davfs2
    prop.append_child("D:quota-available-bytes");
    prop.append_child("D:quota-used-bytes");
#endif
    
#if 0
    prop.append_child("D:lockdiscovery");
    pugi::xml_node lock = prop.append_child("D:supportedlock");

    pugi::xml_node lockentry = lock.append_child("D:lockentry");
    lockentry.append_child("D:lockscope").append_child("D:exclusive");
    lockentry.append_child("D:locktype").append_child("D:write");

    lockentry = lock.append_child("D:lockentry");
    lockentry.append_child("D:lockscope").append_child("D:shared");
    lockentry.append_child("D:locktype").append_child("D:write");
#endif
  }

  
  IWebDavBucket::File::File(const std::string& displayName) :
    Resource(displayName),
    hasContentLength_(false),
    contentLength_(0),
    mime_(MimeType_Binary)
  {
  }

  
  void IWebDavBucket::File::SetContentLength(uint64_t contentLength)
  {
    hasContentLength_ = true;
    contentLength_ = contentLength;
  }


  void IWebDavBucket::File::Format(pugi::xml_node& node,
                                   const std::string& parentPath) const
  {
    std::string href;
    std::vector<std::string> pathTokens;

    Toolbox::SplitString(pathTokens, parentPath, '/');
    pathTokens.push_back(GetDisplayName());

    Toolbox::UriEncode(href, pathTokens);
    href = AddLeadingSlash(href);

    FormatInternal(node, href, GetDisplayName(), GetCreationTime(), GetModificationTime());

    pugi::xml_node prop = node.first_element_by_path("D:propstat/D:prop");
    prop.append_child("D:resourcetype");

    std::string s = EnumerationToString(mime_);
    prop.append_child("D:getcontenttype").append_child(pugi::node_pcdata).set_value(s.c_str());

    if (hasContentLength_)
    {
      s = boost::lexical_cast<std::string>(contentLength_);
      prop.append_child("D:getcontentlength").append_child(pugi::node_pcdata).set_value(s.c_str());
    }
  }


  void IWebDavBucket::Folder::Format(pugi::xml_node& node,
                                     const std::string& parentPath) const
  {
    std::string href;
    std::vector<std::string> pathTokens;

    Toolbox::SplitString(pathTokens, parentPath, '/');
    pathTokens.push_back(GetDisplayName());

    Toolbox::UriEncode(href, pathTokens);
    href = AddLeadingSlash(href);

    FormatInternal(node, href, GetDisplayName(), GetCreationTime(), GetModificationTime());
        
    pugi::xml_node prop = node.first_element_by_path("D:propstat/D:prop");
    prop.append_child("D:resourcetype").append_child("D:collection");

    //prop.append_child("D:getcontenttype").append_child(pugi::node_pcdata).set_value("httpd/unix-directory");
  }


  IWebDavBucket::Collection::~Collection()
  {
    for (std::list<Resource*>::iterator it = resources_.begin(); it != resources_.end(); ++it)
    {
      assert(*it != NULL);
      delete(*it);
    }
  }


  void IWebDavBucket::Collection::AddResource(Resource* resource)  // Takes ownership
  {
    if (resource == NULL)
    {
      throw OrthancException(ErrorCode_NullPointer);
    }
    else
    {
      resources_.push_back(resource);
    }
  }


  void IWebDavBucket::Collection::ListDisplayNames(std::set<std::string>& target)
  {
    for (std::list<Resource*>::iterator it = resources_.begin(); it != resources_.end(); ++it)
    {
      assert(*it != NULL);
      target.insert((*it)->GetDisplayName());
    }
  }


  void IWebDavBucket::Collection::Format(std::string& target,
                                         const std::string& parentPath) const
  {
    pugi::xml_document doc;

    pugi::xml_node root = doc.append_child("D:multistatus");
    root.append_attribute("xmlns:D").set_value("DAV:");

    {
      pugi::xml_node self = root.append_child();

      std::vector<std::string> tokens;
      Toolbox::SplitUriComponents(tokens, parentPath);
      
      std::string folder;
      if (!tokens.empty())
      {
        folder = tokens.back();
      }
       
      std::string href;
      Toolbox::UriEncode(href, tokens);
      href = AddTrailingSlash(AddLeadingSlash(href));

      boost::posix_time::ptime now = GetNow();
      FormatInternal(self, href, folder, now, now);

      pugi::xml_node prop = self.first_element_by_path("D:propstat/D:prop");
      prop.append_child("D:resourcetype").append_child("D:collection");
    }

    for (std::list<Resource*>::const_iterator
           it = resources_.begin(); it != resources_.end(); ++it)
    {
      assert(*it != NULL);
      pugi::xml_node n = root.append_child();
      (*it)->Format(n, parentPath);
    }

    pugi::xml_node decl = doc.prepend_child(pugi::node_declaration);
    decl.append_attribute("version").set_value("1.0");
    decl.append_attribute("encoding").set_value("UTF-8");

    Toolbox::XmlToString(target, doc);
  }


  void IWebDavBucket::AnswerFakedProppatch(HttpOutput& output,
                                           const std::string& uri)
  {
    /**
     * This is a fake implementation. The goal is to make happy the
     * WebDAV clients that set properties (such as Windows >= 7).
     **/
            
    pugi::xml_document doc;

    pugi::xml_node root = doc.append_child("D:multistatus");
    root.append_attribute("xmlns:D").set_value("DAV:");

    pugi::xml_node response = root.append_child("D:response");
    response.append_child("D:href").append_child(pugi::node_pcdata).set_value(uri.c_str());

    response.append_child("D:propstat");

    pugi::xml_node decl = doc.prepend_child(pugi::node_declaration);
    decl.append_attribute("version").set_value("1.0");
    decl.append_attribute("encoding").set_value("UTF-8");

    std::string s;
    Toolbox::XmlToString(s, doc);

    output.AddHeader("Content-Type", "application/xml");
    output.SendStatus(HttpStatus_207_MultiStatus, s);    
  }


  void IWebDavBucket::AnswerFakedLock(HttpOutput& output,
                                      const std::string& uri)
  {
    /**
     * This is a fake implementation. No lock is actually
     * created. The goal is to make happy the WebDAV clients
     * that use locking (such as Windows >= 7).
     **/
            
    pugi::xml_document doc;

    pugi::xml_node root = doc.append_child("D:prop");
    root.append_attribute("xmlns:D").set_value("DAV:");

    pugi::xml_node activelock = root.append_child("D:lockdiscovery").append_child("D:activelock");
    activelock.append_child("D:locktype").append_child("D:write");
    activelock.append_child("D:lockscope").append_child("D:exclusive");
    activelock.append_child("D:depth").append_child(pugi::node_pcdata).set_value("0");
    activelock.append_child("D:timeout").append_child(pugi::node_pcdata).set_value("Second-3599");

    activelock.append_child("D:lockroot").append_child("D:href")
      .append_child(pugi::node_pcdata).set_value(uri.c_str());
    activelock.append_child("D:owner");

    std::string token = Toolbox::GenerateUuid();
    boost::erase_all(token, "-");
    token = "opaquelocktoken:0x" + token;
            
    activelock.append_child("D:locktoken").append_child("D:href").
      append_child(pugi::node_pcdata).set_value(token.c_str());
            
    pugi::xml_node decl = doc.prepend_child(pugi::node_declaration);
    decl.append_attribute("version").set_value("1.0");
    decl.append_attribute("encoding").set_value("UTF-8");

    std::string s;
    Toolbox::XmlToString(s, doc);

    output.AddHeader("Lock-Token", token);  // Necessary for davfs2
    output.AddHeader("Content-Type", "application/xml");
    output.SendStatus(HttpStatus_201_Created, s);
  }


  void IWebDavBucket::AnswerFakedUnlock(HttpOutput& output)
  {
    output.SendStatus(HttpStatus_204_NoContent);
  }
}
