/*
 *
 *    soniK digital audio editor
 *    Copyright (C) 2003-2006  Robert Walker <rob@tenfoot.org.uk>
 *
 *    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 2 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, write to the Free Software
 *    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */
#include "edit.h"
#include "data.h"
#include "partwidget.h"
#include "editapplydialog.h"

#include <kstandarddirs.h>
#include <kdebug.h>

#include <qhbox.h>
#include <qvbox.h>
#include <qtextstream.h>
#include <qdom.h>
#include <qfile.h>

using Sonik::Edit;

namespace
{
  static const std::size_t kBlocksize = 1024;
}


bool operator==(const Sonik::Edit::Preset& a, const Sonik::Edit::Preset& b)
{
  if (a.size() != b.size())
    return false;

  Sonik::Edit::Preset::const_iterator ap = a.begin();
  for ( ; ap != a.end(); ++ap)
  {
    Sonik::Edit::Preset::const_iterator bp = b.find(ap.key());
    if (bp == b.end() ||
        (*bp) != (*ap))
      return false;
  }

  return true;
}


bool operator!=(const Sonik::Edit::Preset& a, const Sonik::Edit::Preset& b)
{
  return !operator==(a, b);
}


Edit::PresetManager::PresetManager(const QString& editorId, KInstance* instance)
  : mEditorId(editorId),
    mInstance(instance),
    mDirty(false)
{
  loadPresets();
}


Edit::PresetManager::~PresetManager()
{
  savePresets();
}


QStringList Edit::PresetManager::presetNames()
{
  return mPresets.keys();
}


Edit::Preset Edit::PresetManager::preset(const QString& presetName)
{
  return mPresets[presetName];
}


void Edit::PresetManager::setPreset(const QString& presetName,
                                    Edit::Preset preset)
{
  mPresets[presetName] = preset;
  mDirty = true;
}


void Edit::PresetManager::loadPresets()
{
  mPresets.clear();

  QString userFileName = userFile();
  QStringList presetFileNames = presetFiles();

  QStringList::const_iterator file = presetFileNames.begin();
  for ( ; file != presetFileNames.end(); ++file)
  {
    if ((*file) != userFileName)
      loadPresetFile(*file, mPresets);
  }

  loadPresetFile(userFileName, mPresets);
}


void Edit::PresetManager::savePresets()
{
  if (!mDirty)
    return;

  QString userFileName = userFile();
  QStringList presetFileNames = presetFiles();

  // load system presets
  Presets systemPresets;
  QStringList::const_iterator file = presetFileNames.begin();
  for ( ; file != presetFileNames.end(); ++file)
  {
    if ((*file) != userFileName)
      loadPresetFile(*file, systemPresets);
  }

  // generate differences in user file
  Presets userPresets;
  QStringList  deletedPresets;

  generateDeltas(mPresets, systemPresets, userPresets, deletedPresets);

  // save user files
  savePresetFile(userFileName, userPresets, deletedPresets);

  mDirty = false;
}


QStringList Edit::PresetManager::presetFiles()
{
  return mInstance->dirs()->findAllResources(
    "data",
    QString(mInstance->instanceName()) + "/presets/" + mEditorId + ".preset",
    true, false
    );
}


QString Edit::PresetManager::userFile()
{
  return mInstance->dirs()->saveLocation(
    "data", QString(mInstance->instanceName()) + "/presets/"
    ) + mEditorId + ".preset";
}


void Edit::PresetManager::loadPresetFile(const QString& filename,
                                         Presets& presets)
{
  QDomDocument doc("presets");
  QFile file(filename);

  if (!file.open(IO_ReadOnly))
    return;

  if (!doc.setContent(&file))
  {
    file.close();
    return;
  }
  file.close();

  QDomElement docElem = doc.documentElement();

  QDomNode n = docElem.firstChild();
  while( !n.isNull() )
  {
    QDomElement ePreset = n.toElement();
    // TODO: ensure element is preset
    if(!ePreset.isNull())
    {
      QString presetName = ePreset.attribute("name");
      QString presetDeleted = ePreset.attribute("deleted");

      if (!presetName.isNull())
      {
        if (!presetDeleted.isNull() && presetDeleted == "true")
        {
          presets.erase(presetName);
        }
        else
        {
          Preset& preset = presets[presetName];

          QDomNode n2 = ePreset.firstChild();
          while (!n2.isNull())
          {
            QDomElement eParam = n2.toElement();
            // TODO: ensure element is param
            if (!eParam.isNull())
            {
              QString param = eParam.attribute("name");
              QString valStr = eParam.attribute("value");
              double   value = 0.0f;
              QTextIStream valStream(&valStr);
              valStream >> value;

              preset[param] = value;
            }

            n2 = n2.nextSibling();
          }

          // TODO: load translations of name
        }
      }
    }
    n = n.nextSibling();
  }
}


void Edit::PresetManager::savePresetFile(const QString& filename,
                                         const Presets& presets,
                                         const QStringList& deletedPresets)
{
  // TODO: express parameter deletions

  QDomDocument doc("presets");
  QDomElement root = doc.createElement("presets");
  doc.appendChild(root);

  // build XML doc
  Presets::const_iterator preset = presets.begin();
  for ( ; preset != presets.end(); ++preset)
  {
    QDomElement presetElement = doc.createElement("preset");
    root.appendChild(presetElement);
    presetElement.setAttribute("name", preset.key());

    Preset::const_iterator param = (*preset).begin();
    for ( ; param != (*preset).end(); ++param)
    {
      QDomElement paramElement = doc.createElement("param");
      presetElement.appendChild(paramElement);
      paramElement.setAttribute("name", param.key());
      paramElement.setAttribute("value", (*param));
    }
  }

  QStringList::const_iterator delPreset = deletedPresets.begin();
  for ( ; delPreset != deletedPresets.end(); ++delPreset)
  {
    QDomElement presetElement = doc.createElement("preset");
    root.appendChild(presetElement);
    presetElement.setAttribute("name", *delPreset);
    presetElement.setAttribute("deleted", "true");
  }

  // TODO: avoid (& delete) empty file

  QFile file(filename);

  if (!file.open(IO_WriteOnly))
  {
    // TODO: error
    return;
  }

  QTextStream stream(&file);
  stream << doc.toString();

  file.close();
}


void Edit::PresetManager::generateDeltas(const Presets& current,
                                         const Presets& base,
                                         Presets&       changed,
                                         QStringList&   deleted)
{
  changed.clear();
  Presets::const_iterator preset = current.begin();
  for ( ; preset != current.end(); ++preset)
  {
    Presets::const_iterator basePreset = base.find(preset.key());

    if (basePreset == base.end() ||
        (*preset) != (*basePreset))
      changed[preset.key()] = (*preset);
    // TODO: parameter level granularity
    // TODO: deleted parameters
  }

  deleted.clear();
  preset = base.begin();
  for ( ; preset != base.end(); ++preset)
  {
    if (current.find(preset.key()) == current.end())
      deleted.push_back(preset.key());
  }
}


Edit::Processor::~Processor()
{
}


bool Edit::Processor::prepare(uint8_t /*channels*/,
                              std::size_t /*len*/,
                              uint32_t /*sampleRate*/,
                              std::size_t /*blocksize*/)
{
  return true;
}


void Edit::Processor::apply(SampleSegment& /*seg*/)
{
}


void Edit::Processor::cleanup()
{
}


Edit::PluginContext::~PluginContext()
{
  for (UiControlPtrList::iterator it = controls.begin();
       it != controls.end();
       ++it)
    delete *it;
  controls.clear();
}


Edit::Edit(const QString& pluginName,  const QString& displayName,
           QObject* parent, const char* name,
           const QStringList& args)
  : Sonik::Plugin(pluginName, displayName, parent, name, args)
{
}

Edit::~Edit()
{
}


void Edit::init(Data* data,
                PartWidget* widget,
                ActionManager* actionManager)
{
  mData          = data;
  mWidget        = widget;
  mActionManager = actionManager;
}


void Edit::pluginDialog(const QString& editorId,
                        QWidget* widget,
                        const QString& caption,
                        const char* applySlot,
                        PluginContext* context)
{
  KXMLGUIClient* xmlParent = dynamic_cast<KXMLGUIClient*>(parent());
  assert(xmlParent);

  EditApplyDialog* dialog = new EditApplyDialog(
    context, new PresetManager(editorId, xmlParent->instance()), mWidget);
  dialog->setWidget(widget);
  dialog->setCaption(caption);

  if (applySlot)
    connect(dialog, SIGNAL(applyPlugin(Sonik::Edit::PluginContext*)),
            this, applySlot);

  dialog->show();
}


bool Edit::apply(Edit::Processor& p, bool selectionOnly)
{
  off_t  start;
  std::size_t length;

  if (selectionOnly && mWidget->selectionLength() > 0)
  {
    start = mWidget->selectionStart();
    length = mWidget->selectionLength();
  }
  else
  {
    start = 0;
    length = mData->length();
  }

  if (!p.prepare(mData->channels(), length, mData->sampleRate(), kBlocksize))
  {
    p.cleanup();
    return false;
  }

  runPlugin(p, start, length);

  p.cleanup();

  return true;
}


bool Edit::generate(Edit::Processor& p, InsertPosition position, size_t length)
{
  off_t start = 0;

  if (length == 0)
    return false;

  switch (position)
  {
    case kReplaceAll:
      start = 0;

    case kInsertStart:
      start = 0;
      break;

    case kInsertCursor:
      start = mWidget->selectionStart();
      break;

    case kInsertEnd:
      start = mData->length();
      break;

    case kOverwriteCursor:
      start = mWidget->selectionStart();
      break;

    case kReplaceSelection:
      start = mWidget->selectionStart();
      break;
  }

  if (!p.prepare(mData->channels(), length, mData->sampleRate(), kBlocksize))
  {
    p.cleanup();
    return false;
  }

  // remove old data after plugin init, so we don't courrupt if it fails
  if (position == kOverwriteCursor || position == kReplaceAll)
    mData->remove(start, mData->length() - start);
  else if (position ==  kReplaceSelection)
    mData->remove(start, mWidget->selectionLength());

  mData->insert(start, length);

  runPlugin(p, start, length);

  p.cleanup();

  mWidget->select(start, length);

  return true;
}


void Edit::runPlugin(Edit::Processor& p, off_t start, size_t length)
{
  SampleSegment seg(mData->channels(), kBlocksize);

  off_t pos = start;
  std::size_t left = length;
  while (left)
  {
    std::size_t cnt = (left <= kBlocksize) ? left : kBlocksize;

    mData->data(pos, cnt, seg);
    p.apply(seg);
    mData->setData(pos, seg);

    left -= cnt;
    pos  += cnt;
  }
}
