// -*- mode: c++; c-file-style: "linux"; c-basic-offset: 2; indent-tabs-mode:
// nil -*-
//
//  Copyright (C) 2004-2015 Andrej Vodopivec <andrej.vodopivec@gmail.com>
//  Copyright (C) 2015      Gunter Königsmann <wxMaxima@physikbuch.de>
//  Copyright (C) 2020      Kuba Ober <kuba@bertec.com>
//
//  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
//
//  SPDX-License-Identifier: GPL-2.0+

#include "OutCommon.h"
#include "Dirstructure.h"
#include "cells/GroupCell.h"
#include "worksheet/Worksheet.h"
#include <cmath>
#include <wx/clipbrd.h>
#include <wx/filename.h>
#include <wx/wfstream.h>

namespace {

/*! A private (mode 0700) subdirectory of the user's own config directory, for
  the throwaway files this class creates while rendering an export for the
  clipboard (there is no in-memory alternative: wxSVGFileDC and friends only
  know how to write to a real path).

  Unlike the shared system temp directory these otherwise land in, no other
  unprivileged user has write access here, so nobody can race the file's
  creation with a symlink planted under the name it happens to get.

  \return an empty string if the directory couldn't be created (e.g.
          UserConfDir() itself is unusable), so the caller falls back to
          wxFileName::CreateTempFileName()'s own default location.
*/
wxString PrivateTempDir() {
  wxString dir = Dirstructure::UserConfDir();
  if (dir.IsEmpty())
    return {};
  if (!dir.EndsWith(wxFileName::GetPathSeparator()))
    dir += wxFileName::GetPathSeparator();
  dir += wxS("tmp");
  if (!wxFileName::DirExists(dir) &&
      !wxFileName::Mkdir(dir, wxS_IRUSR | wxS_IWUSR | wxS_IXUSR, wxPATH_MKDIR_FULL))
    return {};
  return dir + wxFileName::GetPathSeparator();
}

wxString MakeTempFilename(const wxString &prefix) {
    return wxFileName::CreateTempFileName(PrivateTempDir() + prefix);
}

} // namespace

wxString OutCommon::AccessibleText(const Cell *tree) {
  if (!tree)
    return {};

  const wxString text = tree->ListToString();
  wxString label;
  label.reserve(text.length());
  bool pendingSpace = false;
  for (const wxUniChar c : text) {
    if (wxIsspace(c))
      pendingSpace = true;
    else {
      if (pendingSpace && !label.IsEmpty())
        label += wxS(' ');
      pendingSpace = false;
      label += c;
    }
  }
  return label;
}

OutCommon::OutCommon(const Configuration * const *configuration, const wxString &filename,
                     int fullWidth, double scale)
    : m_tempFilename(MakeTempFilename(wxS("wxmaxima_size_"))),
      m_filename(filename.empty()
                 ? MakeTempFilename(wxS("wxmaxima_"))
                 : filename),
      m_configuration(configuration), m_scale(scale), m_fullWidth(fullWidth) {
    m_thisconfig.ShowCodeCells(m_oldconfig->ShowCodeCells());
    m_thisconfig.SetWorkSheet((*configuration)->GetWorkSheet());
    m_thisconfig.SetDocumentCellPointers((*configuration)->GetDocumentCellPointers());
    m_thisconfig.SetViewCellPointers((*configuration)->GetViewCellPointers());

    //    *m_configuration = &m_thisconfig;
    m_thisconfig.SetZoomFactor_temporarily(1);
    m_thisconfig.FontChanged();

    // The last time I tried it the vertical positioning of the elements
    // of a big unicode parenthesis wasn't accurate enough in emf to be
    // usable. Also the probability was high that the right font wasn't
    // available in inkscape.
    m_thisconfig.SetParenthesisDrawMode(Configuration::handdrawn);
}

OutCommon::OutCommon(const Configuration * const *configuration, int fullWidth, double scale)
    : OutCommon(configuration, {}, fullWidth, scale) {}

OutCommon::~OutCommon() {
    if (wxFileExists(m_tempFilename)) {

        if (!wxRemoveFile(m_tempFilename))
            wxLogMessage(_("Cannot remove the file %s"), m_tempFilename.mb_str());
    }
    //    *m_configuration = m_oldconfig;
    //    (*m_configuration)->FontChanged();
}

wxSize OutCommon::GetScaledSize() const {
    int w = std::lround(m_size.x * m_scale);
    int h = std::lround(m_size.y * m_scale);
    return {w, h};
}

wxSize OutCommon::GetInvScaledSize() const {
    int w = std::lround(m_size.x / m_scale);
    int h = std::lround(m_size.y / m_scale);
    return {w, h};
}

bool OutCommon::PrepareLayout(Cell *tree) {
    wxASSERT(m_recalculationDc);
    if (!tree)
        return false;

    tree->SetConfigurationList(&m_thisconfig);
    m_thisconfig.ClipToDrawRegion(false);
    tree->FontsChangedList();
    tree->ResetSize();
    if(m_recalculationDc)
        m_thisconfig.SetRecalcContext(*m_recalculationDc);

    if (tree->GetType() != MC_TYPE_GROUP) {
        Recalculate(tree);
        tree->UnBreakUpCells();
        Recalculate(tree);
        tree->BreakUpCells();
        Recalculate(tree);
        tree->BreakLines_List();
        Recalculate(tree);
    } else {
        for (GroupCell &tmp : OnList(dynamic_cast<GroupCell *>(tree)))
            tmp.Recalculate();
    }

    if(m_recalculationDc == NULL)
        return false;
    if (!m_recalculationDc->IsOk())
        return false;

    GetMaxPoint(tree, &m_size.x, &m_size.y);
    m_thisconfig.SetUpdateRegion(wxRect(0, 0, m_size.x, m_size.y));
    return true;
}

void OutCommon::Recalculate(Cell *tree) const {
    auto fontsize = m_thisconfig.GetDefaultFontSize();
    auto mathFontsize = m_thisconfig.GetMathFontSize();

    for (Cell &tmp : OnList(tree))
        tmp.Recalculate(tmp.IsMath() ? mathFontsize : fontsize);
}

void OutCommon::BreakLines(Cell *tree) const {
    int currentWidth = 0;
    int fullWidth = m_fullWidth * m_scale;

    for (Cell &tmp : OnDrawList(tree)) {
        if (tmp.IsBrokenIntoLines())
            continue;

        // if (tmp.SoftLineBreak(false))
        //     // Note: This ResetData() call was unconditional in EMFout and SVGout.
        //     // The condition check was only performed in BitmapOut. That was likely
        //     // an omission. This note is here in case bugs were found in this area.
        //     tmp.ResetSize_Recursively();

        if (tmp.BreakLineHere() || (currentWidth + tmp.GetWidth() >= fullWidth)) {
            currentWidth = tmp.GetWidth();
            tmp.SoftLineBreak(true);
        } else
            currentWidth += (tmp.GetWidth());
    }
}

void OutCommon::GetMaxPoint(Cell *tree, int *width, int *height) const {
    int currentHeight = 0;
    int currentWidth = 0;
    *width = 0;
    *height = 0;
    bool bigSkip = false;
    bool firstCell = true;

    for (const Cell &tmp : OnDrawList(tree)) {
        if (tmp.IsBrokenIntoLines())
            continue;

        if (tmp.BreakLineHere() || firstCell) {
            firstCell = false;
            currentHeight += tmp.GetHeightList();
            if (bigSkip)
                // Note: This skip was observerd in EMFout and SVGout, but not
                // BitmapOut.
                currentHeight += MC_LINE_SKIP;
            *height = currentHeight;
            currentWidth = tmp.GetWidth();
            *width = std::max(currentWidth, *width);
        } else {
            currentWidth += (tmp.GetWidth());
            *width = std::max(currentWidth, *width);
        }
        bigSkip = tmp.HasBigSkip();
    }
}

void OutCommon::Draw(Cell *tree) {
    wxPoint point;
    point.x = 0;
    point.y = tree->GetCenterList();
    int drop = tree->GetMaxDrop();

    for (Cell &tmp : OnDrawList(tree)) {
        const Cell *const next = tmp.GetNext();
        if (!tmp.IsBrokenIntoLines()) {
            tmp.SetCurrentPoint(point);
            tmp.Draw(m_recalculationDc, m_recalculationDc);
            if (next && next->BreakLineHere()) {
                point.x = 0;
                point.y += drop + next->GetCenterList();
                if (tmp.HasBigSkip())
                    // Note: This skip was observerd in EMFout and SVGout, but not
                    // BitmapOut.
                    point.y += MC_LINE_SKIP;
                drop = next->GetMaxDrop();
            } else
                point.x += tmp.GetWidth();
        } else {
            if (next && next->BreakLineHere()) {
                point.x = 0;
                point.y += drop + next->GetCenterList();
                if (tmp.HasBigSkip())
                    // Note: This skip was observerd in EMFout and SVGout, but not
                    // BitmapOut.
                    point.y += MC_LINE_SKIP;
                drop = next->GetMaxDrop();
            }
        }
    }

    // Update the bitmap's size information.
    m_ppi = m_thisconfig.GetRecalcDC()->GetPPI();
    m_ppi.x *= m_scale;
    m_ppi.y *= m_scale;
}

void OutCommon::BreakUpCells(Cell *tree) const {
    int fullWidth = m_fullWidth * m_scale;
    auto fontsize = m_thisconfig.GetDefaultFontSize();
    auto mathFontsize = m_thisconfig.GetMathFontSize();

    for (Cell &tmp : OnDrawList(tree)) {
        if (tmp.GetWidth() > fullWidth && tmp.BreakUp())
            tmp.Recalculate(tmp.IsMath() ? mathFontsize : fontsize);
    }
}

bool OutCommon::ToClipboard(const wxDataFormat &format) {
    wxASSERT_MSG(!wxTheClipboard->IsOpened(),
                 _("Bug: The clipboard is already opened"));
    if (wxTheClipboard->Open()) {
        bool res = wxTheClipboard->SetData(GetDataObject(format).release());
        wxTheClipboard->Close();
        m_filename.clear();
        return res;
    }
    return false;
}

std::unique_ptr<OutCommon::DataObject>
OutCommon::GetDataObject(const wxDataFormat &format) {
    constexpr auto chunkSize = 8192;
    wxMemoryBuffer contents;
    {
        wxFileInputStream str(m_filename);
        if (str.IsOk())
            // The IsOk() term matters: on a read ERROR (as opposed to EOF)
            // Eof() stays false and LastRead() 0, so the loop would spin
            // forever without appending anything.
            while (str.IsOk() && !str.Eof()) {
                auto *buf = contents.GetAppendBuf(chunkSize);
                str.Read(buf, chunkSize);
                contents.UngetAppendBuf(str.LastRead());
            }
    }

    if ((!m_filename.empty()) && wxFileExists(m_filename)) {
        wxRemoveFile(m_filename);
    }
    m_filename.clear();

    return std::unique_ptr<DataObject>(new DataObject(format, contents));
}

OutCommon::DataObject::DataObject(const wxDataFormat &format,
                                  const wxMemoryBuffer &data)
    : wxCustomDataObject(format), m_databuf(0)
      // We can't point m_databuf to data here, since TakeData calls Free() and will
      // ruin it!
{
    // cppcheck-suppress useInitializationList
    m_databuf = data;
}

bool OutCommon::DataObject::GetDataHere(void *buf) const {
    // wx calls GetDataSize() (== m_databuf.GetDataLen()) first and allocates
    // buf accordingly, so the destination is always exactly large enough.
    memcpy(buf, m_databuf.GetData(), m_databuf.GetDataLen()); // flawfinder: ignore
    return true;
}

size_t OutCommon::DataObject::GetDataSize() const {
    return m_databuf.GetDataLen();
}

void OutCommon::DataObject::Free() { m_databuf.Clear(); }
