/*
 * ModDoc.h
 * --------
 * Purpose: Converting between various module formats.
 * Notes  : (currently none)
 * Authors: OpenMPT Devs
 * The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
 */


#pragma once

#include "openmpt/all/BuildSettings.hpp"

#include "Sndfile.h"
#include "../common/misc_util.h"
#include "Undo.h"
#include "Notification.h"
#include "UpdateHints.h"
#include <time.h>

OPENMPT_NAMESPACE_BEGIN

class EncoderFactoryBase;
class CChildFrame;

/////////////////////////////////////////////////////////////////////////
// Split Keyboard Settings (pattern editor)

struct SplitKeyboardSettings
{
	enum
	{
		splitOctaveRange = 9,
	};

	bool IsSplitActive() const { return (octaveLink && (octaveModifier != 0)) || (splitInstrument != 0) || (splitVolume != 0); }

	int octaveModifier = 0;	// determines by how many octaves the notes should be transposed up or down
	ModCommand::NOTE splitNote = NOTE_MIDDLEC - 1;
	ModCommand::INSTR splitInstrument = 0;
	ModCommand::VOL splitVolume = 0;
	bool octaveLink = false;	// apply octaveModifier
};

enum InputTargetContext : int8;


struct LogEntry
{
	LogLevel level;
	mpt::ustring message;
	LogEntry() : level(LogInformation) {}
	LogEntry(LogLevel l, const mpt::ustring &m) : level(l), message(m) {}
};


enum LogMode
{
	LogModeInstantReporting,
	LogModeGather,
};


class ScopedLogCapturer
{
private:
	CModDoc &m_modDoc;
	LogMode m_oldLogMode;
	CString m_title;
	CWnd *m_pParent;
	bool m_showLog;
public:
	ScopedLogCapturer(CModDoc &modDoc, const CString &title = {}, CWnd *parent = nullptr, bool showLog = true);
	~ScopedLogCapturer();
	void ShowLog(bool force = false);
	void ShowLog(const CString &preamble, bool force = false);
	[[deprecated]] void ShowLog(const std::string &preamble, bool force = false);
	void ShowLog(const mpt::ustring &preamble, bool force = false);
};


struct PlayNoteParam
{
	std::bitset<128> *m_notesPlaying = nullptr;
	SmpLength m_loopStart = 0, m_loopEnd = 0, m_sampleOffset = 0;
	int32 m_volume = -1;
	SAMPLEINDEX m_sample = 0;
	INSTRUMENTINDEX m_instr = 0;
	CHANNELINDEX m_currentChannel = CHANNELINDEX_INVALID;
	ModCommand::NOTE m_note;

	PlayNoteParam(ModCommand::NOTE note) : m_note(note) { }

	PlayNoteParam& LoopStart(SmpLength loopStart) { m_loopStart = loopStart; return *this; }
	PlayNoteParam& LoopEnd(SmpLength loopEnd) { m_loopEnd = loopEnd; return *this; }
	PlayNoteParam& Offset(SmpLength sampleOffset) { m_sampleOffset = sampleOffset; return *this; }

	PlayNoteParam& Volume(int32 volume) { m_volume = volume; return *this; }
	PlayNoteParam& Sample(SAMPLEINDEX sample) { m_sample = sample; return *this; }
	PlayNoteParam& Instrument(INSTRUMENTINDEX instr) { m_instr = instr; return *this; }
	PlayNoteParam& Channel(CHANNELINDEX channel) { m_currentChannel = channel; return *this; }

	PlayNoteParam& CheckNNA(std::bitset<128> &notesPlaying) { m_notesPlaying = &notesPlaying; return *this; }
};


enum class RecordGroup : uint8
{
	NoGroup = 0,
	Group1 = 1,
	Group2 = 2,
};


class CModDoc final : public CDocument
{
protected:
	friend ScopedLogCapturer;
	mutable std::vector<LogEntry> m_Log;
	LogMode m_LogMode = LogModeInstantReporting;
	CSoundFile m_SndFile;

	HWND m_hWndFollow = nullptr;
	FlagSet<Notification::Type, uint16> m_notifyType;
	Notification::Item m_notifyItem = 0;
	CSize m_szOldPatternScrollbarsPos = { -10, -10 };

	CPatternUndo m_PatternUndo;
	CSampleUndo m_SampleUndo;
	CInstrumentUndo m_InstrumentUndo;
	SplitKeyboardSettings m_SplitKeyboardSettings;	// this is maybe not the best place to keep them, but it should do the job
	time_t m_creationTime;

	std::atomic<bool> m_modifiedAutosave = false; // Modified since last autosave?

public:
	class NoteToChannelMap : public std::array<CHANNELINDEX, NOTE_MAX - NOTE_MIN + 1>
	{
	public:
		NoteToChannelMap() { fill(CHANNELINDEX_INVALID); }
	};
	NoteToChannelMap m_noteChannel;	// Note -> Preview channel assignment

	bool m_ShowSavedialog = false;
	bool m_bHasValidPath = false; //becomes true if document is loaded or saved.

protected:
	// Note-off event buffer for MIDI sustain pedal
	std::array<std::vector<uint32>, 16> m_midiSustainBuffer;
	std::array<std::bitset<128>, 16> m_midiPlayingNotes;
	std::bitset<16> m_midiSustainActive;

	std::bitset<MAX_BASECHANNELS> m_bsMultiRecordMask;
	std::bitset<MAX_BASECHANNELS> m_bsMultiSplitRecordMask;

protected: // create from serialization only
	CModDoc();
	DECLARE_DYNCREATE(CModDoc)

// public members
public:
	CSoundFile &GetSoundFile() { return m_SndFile; }
	const CSoundFile &GetSoundFile() const { return m_SndFile; }

#if MPT_COMPILER_CLANG
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Woverloaded-virtual"
#endif // MPT_COMPILER_CLANG
	bool IsModified() const { return m_bModified != FALSE; }	// Work-around: CDocument::IsModified() is not const...
#if MPT_COMPILER_CLANG
#pragma clang diagnostic pop
#endif // MPT_COMPILER_CLANG
	void SetModified(bool modified = true);
	bool ModifiedSinceLastAutosave();
	void SetShowSaveDialog(bool b) { m_ShowSavedialog = b; }
	void PostMessageToAllViews(UINT uMsg, WPARAM wParam = 0, LPARAM lParam = 0);
	void SendNotifyMessageToAllViews(UINT uMsg, WPARAM wParam = 0, LPARAM lParam = 0);
	void SendMessageToActiveView(UINT uMsg, WPARAM wParam = 0, LPARAM lParam = 0);
	MODTYPE GetModType() const { return m_SndFile.m_nType; }
	INSTRUMENTINDEX GetNumInstruments() const { return m_SndFile.m_nInstruments; }
	SAMPLEINDEX GetNumSamples() const { return m_SndFile.m_nSamples; }

	// Logging for general progress and error events.
	void AddToLog(LogLevel level, const mpt::ustring &text) const;
	/*[[deprecated]]*/ void AddToLog(const CString &text) const { AddToLog(LogInformation, mpt::ToUnicode(text)); }
	/*[[deprecated]]*/ void AddToLog(const std::string &text) const { AddToLog(LogInformation, mpt::ToUnicode(mpt::Charset::Locale, text)); }
	/*[[deprecated]]*/ void AddToLog(const char *text) const { AddToLog(LogInformation, mpt::ToUnicode(mpt::Charset::Locale, text ? text : "")); }

	const std::vector<LogEntry> & GetLog() const { return m_Log; }
	mpt::ustring GetLogString() const;
	LogLevel GetMaxLogLevel() const;
protected:
	LogMode GetLogMode() const { return m_LogMode; }
	void SetLogMode(LogMode mode) { m_LogMode = mode; }
	void ClearLog();
	UINT ShowLog(const CString &preamble, const CString &title = {}, CWnd *parent = nullptr);
	UINT ShowLog(const CString &title = {}, CWnd *parent = nullptr) { return ShowLog(_T(""), title, parent); }

public:

	void ClearFilePath() { m_strPathName.Empty(); }

	void ViewPattern(UINT nPat, UINT nOrd);
	void ViewSample(UINT nSmp);
	void ViewInstrument(UINT nIns);
	HWND GetFollowWnd() const { return m_hWndFollow; }
	void SetFollowWnd(HWND hwnd);

	void SetNotifications(FlagSet<Notification::Type> type, Notification::Item item = 0) { m_notifyType = type; m_notifyItem = item; }
	FlagSet<Notification::Type, uint16> GetNotificationType() const { return m_notifyType; }
	Notification::Item GetNotificationItem() const { return m_notifyItem; }

	void ActivateWindow();

	void OnSongProperties();

	void PrepareUndoForAllPatterns(bool storeChannelInfo = false, const char *description = "");
	CPatternUndo &GetPatternUndo() { return m_PatternUndo; }
	CSampleUndo &GetSampleUndo() { return m_SampleUndo; }
	CInstrumentUndo &GetInstrumentUndo() { return m_InstrumentUndo; }
	SplitKeyboardSettings &GetSplitKeyboardSettings() { return m_SplitKeyboardSettings; }

	time_t GetCreationTime() const { return m_creationTime; }

// operations
public:
	bool ChangeModType(MODTYPE wType);

	bool ChangeNumChannels(CHANNELINDEX nNewChannels, const bool showCancelInRemoveDlg = true);
	bool RemoveChannels(const std::vector<bool> &keepMask, bool verbose = false);
	void CheckUsedChannels(std::vector<bool> &usedMask, CHANNELINDEX maxRemoveCount = MAX_BASECHANNELS) const;

	CHANNELINDEX ReArrangeChannels(const std::vector<CHANNELINDEX> &fromToArray, const bool createUndoPoint = true);
	SAMPLEINDEX ReArrangeSamples(const std::vector<SAMPLEINDEX> &newOrder);
	INSTRUMENTINDEX ReArrangeInstruments(const std::vector<INSTRUMENTINDEX> &newOrder, deleteInstrumentSamples removeSamples = doNoDeleteAssociatedSamples);
	SEQUENCEINDEX ReArrangeSequences(const std::vector<SEQUENCEINDEX> &newOrder);

	bool ConvertInstrumentsToSamples();
	bool ConvertSamplesToInstruments();
	PLUGINDEX RemovePlugs(const std::vector<bool> &keepMask);
	bool RemovePlugin(PLUGINDEX plugin);

	void ClonePlugin(SNDMIXPLUGIN &target, const SNDMIXPLUGIN &source);
	void AppendModule(const CSoundFile &source);

	// Create a new pattern and, if order position is specified, inserts it into the order list.
	PATTERNINDEX InsertPattern(ROWINDEX rows, ORDERINDEX ord = ORDERINDEX_INVALID);
	SAMPLEINDEX InsertSample();
	INSTRUMENTINDEX InsertInstrument(SAMPLEINDEX sample = SAMPLEINDEX_INVALID, INSTRUMENTINDEX duplicateSource = INSTRUMENTINDEX_INVALID, bool silent = false);
	INSTRUMENTINDEX InsertInstrumentForPlugin(PLUGINDEX plug);
	INSTRUMENTINDEX HasInstrumentForPlugin(PLUGINDEX plug) const;
	void InitializeInstrument(ModInstrument *pIns);
	bool RemoveOrder(SEQUENCEINDEX nSeq, ORDERINDEX nOrd);
	bool RemovePattern(PATTERNINDEX nPat);
	bool RemoveSample(SAMPLEINDEX nSmp);
	bool RemoveInstrument(INSTRUMENTINDEX nIns);

	void ProcessMIDI(uint32 midiData, INSTRUMENTINDEX ins, IMixPlugin *plugin, InputTargetContext ctx);
	CHANNELINDEX PlayNote(PlayNoteParam &params, NoteToChannelMap *noteChannel = nullptr);
	bool NoteOff(UINT note, bool fade = false, INSTRUMENTINDEX ins = INSTRUMENTINDEX_INVALID, CHANNELINDEX currentChn = CHANNELINDEX_INVALID);
	void CheckNNA(ModCommand::NOTE note, INSTRUMENTINDEX ins, std::bitset<128> &playingNotes);
	void UpdateOPLInstrument(SAMPLEINDEX smp);

	bool IsNotePlaying(UINT note, SAMPLEINDEX nsmp = 0, INSTRUMENTINDEX nins = 0);
	bool MuteChannel(CHANNELINDEX nChn, bool bMute);
	bool UpdateChannelMuteStatus(CHANNELINDEX nChn);
	bool MuteSample(SAMPLEINDEX nSample, bool bMute);
	bool MuteInstrument(INSTRUMENTINDEX nInstr, bool bMute);
	// Returns true if toggling the mute status of a channel should set the document as modified given the current module format and settings.
	bool MuteToggleModifiesDocument() const;

	bool SoloChannel(CHANNELINDEX nChn, bool bSolo);
	bool IsChannelSolo(CHANNELINDEX nChn) const;

	bool SurroundChannel(CHANNELINDEX nChn, bool bSurround);
	bool SetChannelGlobalVolume(CHANNELINDEX nChn, uint16 nVolume);
	bool SetChannelDefaultPan(CHANNELINDEX nChn, uint16 nPan);
	bool IsChannelMuted(CHANNELINDEX nChn) const;
	bool IsSampleMuted(SAMPLEINDEX nSample) const;
	bool IsInstrumentMuted(INSTRUMENTINDEX nInstr) const;

	bool NoFxChannel(CHANNELINDEX nChn, bool bNoFx, bool updateMix = true);
	bool IsChannelNoFx(CHANNELINDEX nChn) const;

	RecordGroup GetChannelRecordGroup(CHANNELINDEX channel) const;
	void SetChannelRecordGroup(CHANNELINDEX channel, RecordGroup recordGroup);
	void ToggleChannelRecordGroup(CHANNELINDEX channel, RecordGroup recordGroup);
	void ReinitRecordState(bool unselect = true);

	CHANNELINDEX GetNumChannels() const { return m_SndFile.m_nChannels; }
	UINT GetPatternSize(PATTERNINDEX nPat) const;
	bool IsChildSample(INSTRUMENTINDEX nIns, SAMPLEINDEX nSmp) const;
	INSTRUMENTINDEX FindSampleParent(SAMPLEINDEX sample) const;
	SAMPLEINDEX FindInstrumentChild(INSTRUMENTINDEX nIns) const;
	bool MoveOrder(ORDERINDEX nSourceNdx, ORDERINDEX nDestNdx, bool bUpdate = true, bool bCopy = false, SEQUENCEINDEX nSourceSeq = SEQUENCEINDEX_INVALID, SEQUENCEINDEX nDestSeq = SEQUENCEINDEX_INVALID);
	BOOL ExpandPattern(PATTERNINDEX nPattern);
	BOOL ShrinkPattern(PATTERNINDEX nPattern);

	bool SetDefaultChannelColors() { return SetDefaultChannelColors(0, GetNumChannels()); }
	bool SetDefaultChannelColors(CHANNELINDEX channel) { return SetDefaultChannelColors(channel, channel + 1u); }
	bool SetDefaultChannelColors(CHANNELINDEX minChannel, CHANNELINDEX maxChannel);
	bool SupportsChannelColors() const { return GetModType() & (MOD_TYPE_XM | MOD_TYPE_IT | MOD_TYPE_MPT); }

	bool CopyEnvelope(INSTRUMENTINDEX nIns, EnvelopeType nEnv);
	bool SaveEnvelope(INSTRUMENTINDEX nIns, EnvelopeType nEnv, const mpt::PathString &fileName);
	bool PasteEnvelope(INSTRUMENTINDEX nIns, EnvelopeType nEnv);
	bool LoadEnvelope(INSTRUMENTINDEX nIns, EnvelopeType nEnv, const mpt::PathString &fileName);

	LRESULT ActivateView(UINT nIdView, DWORD dwParam);
	// Notify all views of document updates (GUI thread only)
	void UpdateAllViews(CView *pSender, UpdateHint hint, CObject *pHint=NULL);
	// Notify all views of document updates (for non-GUI threads)
	void UpdateAllViews(UpdateHint hint);
	void GetEditPosition(ROWINDEX &row, PATTERNINDEX &pat, ORDERINDEX &ord);
	LRESULT OnCustomKeyMsg(WPARAM, LPARAM);
	void TogglePluginEditor(UINT m_nCurrentPlugin, bool onlyThisEditor = false);
	void RecordParamChange(PLUGINDEX slot, PlugParamIndex param);
	void LearnMacro(int macro, PlugParamIndex param);
	void SetElapsedTime(ORDERINDEX nOrd, ROWINDEX nRow, bool setSamplePos);
	void SetLoopSong(bool loop);

	// Global settings to pattern effect conversion
	bool GlobalVolumeToPattern();

	bool HasMPTHacks(const bool autofix = false);

	void FixNullStrings();

// Fix: save pattern scrollbar position when switching to other tab
	CSize GetOldPatternScrollbarsPos() const { return m_szOldPatternScrollbarsPos; };
	void SetOldPatternScrollbarsPos( CSize s ){ m_szOldPatternScrollbarsPos = s; };

	void OnFileWaveConvert(ORDERINDEX nMinOrder, ORDERINDEX nMaxOrder);
	void OnFileWaveConvert(ORDERINDEX nMinOrder, ORDERINDEX nMaxOrder, const std::vector<EncoderFactoryBase*> &encFactories);

	// Returns formatted ModInstrument name.
	// [in] bEmptyInsteadOfNoName: In case of unnamed instrument string, "(no name)" is returned unless this
	//                             parameter is true is case which an empty name is returned.
	// [in] bIncludeIndex: True to include instrument index in front of the instrument name, false otherwise.
	CString GetPatternViewInstrumentName(INSTRUMENTINDEX nInstr, bool bEmptyInsteadOfNoName = false, bool bIncludeIndex = true) const;

	// Check if a given channel contains data.
	bool IsChannelUnused(CHANNELINDEX nChn) const;
	// Check whether a sample is used.
	// In sample mode, the sample numbers in all patterns are checked.
	// In instrument mode, it is only checked if a sample is referenced by an instrument (but not if the sample is actually played anywhere)
	bool IsSampleUsed(SAMPLEINDEX sample, bool searchInMutedChannels = true) const;
	// Check whether an instrument is used (only for instrument mode).
	bool IsInstrumentUsed(INSTRUMENTINDEX instr, bool searchInMutedChannels = true) const;

// protected members
protected:

	void InitializeMod();

	CChildFrame *GetChildFrame(); //rewbs.customKeys

// Overrides
	// ClassWizard generated virtual function overrides
	//{{AFX_VIRTUAL(CModDoc)
	public:
	BOOL OnNewDocument() override;
	BOOL OnOpenDocument(LPCTSTR lpszPathName) override;
	BOOL OnSaveDocument(LPCTSTR lpszPathName) override
	{
		return OnSaveDocument(lpszPathName ? mpt::PathString::FromCString(lpszPathName) : mpt::PathString()) ? TRUE : FALSE;
	}
	void OnCloseDocument() override;
	void SafeFileClose();
	bool OnSaveDocument(const mpt::PathString &filename, const bool setPath = true);

#if MPT_COMPILER_CLANG
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Woverloaded-virtual"
#endif // MPT_COMPILER_CLANG
	void SetPathName(const mpt::PathString &filename, BOOL bAddToMRU = TRUE)
	{
		CDocument::SetPathName(filename.ToCString(), bAddToMRU);
	}
#if MPT_COMPILER_CLANG
#pragma clang diagnostic pop
#endif // MPT_COMPILER_CLANG
	mpt::PathString GetPathNameMpt() const
	{
		return mpt::PathString::FromCString(GetPathName());
	}

	BOOL SaveModified() override;
	bool SaveAllSamples(bool showPrompt = true);
	bool SaveSample(SAMPLEINDEX smp);

	BOOL DoSave(LPCTSTR lpszPathName, BOOL /*bSaveAs*/ = TRUE) override
	{
		return DoSave(lpszPathName ? mpt::PathString::FromCString(lpszPathName) : mpt::PathString());
	}
	BOOL DoSave(const mpt::PathString &filename, bool setPath = true);
	void DeleteContents() override;
	//}}AFX_VIRTUAL

	// Get the sample index for the current pattern cell (resolves instrument note maps, etc)
	SAMPLEINDEX GetSampleIndex(const ModCommand &m, ModCommand::INSTR lastInstr = 0) const;
	// Get group (octave) size from given instrument (or sample in sample mode)
	int GetInstrumentGroupSize(INSTRUMENTINDEX instr) const;
	int GetBaseNote(INSTRUMENTINDEX instr) const;
	ModCommand::NOTE GetNoteWithBaseOctave(int noteOffset, INSTRUMENTINDEX instr) const;

	// Convert a linear volume property to decibels
	static CString LinearToDecibels(double value, double valueAtZeroDB);
	// Convert a panning value to a more readable string
	static CString PanningToString(int32 value, int32 valueAtCenter);

	void SerializeViews() const;
	void DeserializeViews();

	// View MIDI Mapping dialog for given plugin and parameter combination.
	void ViewMIDIMapping(PLUGINDEX plugin = PLUGINDEX_INVALID, PlugParamIndex param = 0);

// Implementation
public:
	virtual ~CModDoc();

// Generated message map functions
public:
	//{{AFX_MSG(CModDoc)
	afx_msg void OnFileWaveConvert();
	afx_msg void OnFileMidiConvert();
	afx_msg void OnFileOPLExport();
	afx_msg void OnFileCompatibilitySave();
	afx_msg void OnPlayerPlay();
	afx_msg void OnPlayerStop();
	afx_msg void OnPlayerPause();
	afx_msg void OnPlayerPlayFromStart();
	afx_msg void OnPanic();
	afx_msg void OnEditGlobals();
	afx_msg void OnEditPatterns();
	afx_msg void OnEditSamples();
	afx_msg void OnEditInstruments();
	afx_msg void OnEditComments();
	afx_msg void OnShowCleanup();
	afx_msg void OnShowSampleTrimmer();
	afx_msg void OnSetupZxxMacros();
	afx_msg void OnEstimateSongLength();
	afx_msg void OnApproximateBPM();
	afx_msg void OnUpdateXMITMPTOnly(CCmdUI *p);
	afx_msg void OnUpdateHasEditHistory(CCmdUI *p);
	afx_msg void OnUpdateHasMIDIMappings(CCmdUI *p);
	afx_msg void OnUpdateCompatExportableOnly(CCmdUI *p);
	afx_msg void OnPatternRestart() { OnPatternRestart(true); } //rewbs.customKeys
	afx_msg void OnPatternRestart(bool loop); //rewbs.customKeys
	afx_msg void OnPatternPlay(); //rewbs.customKeys
	afx_msg void OnPatternPlayNoLoop(); //rewbs.customKeys
	afx_msg void OnViewEditHistory();
	afx_msg void OnViewMPTHacks();
	afx_msg void OnViewTempoSwingSettings();
	afx_msg void OnSaveCopy();
	afx_msg void OnSaveTemplateModule();
	afx_msg void OnAppendModule();
	afx_msg void OnViewMIDIMapping() { ViewMIDIMapping(); }
	afx_msg void OnChannelManager();
	//}}AFX_MSG
	DECLARE_MESSAGE_MAP()
private:

	void ChangeFileExtension(MODTYPE nNewType);
	CHANNELINDEX FindAvailableChannel() const;
};

/////////////////////////////////////////////////////////////////////////////

//{{AFX_INSERT_LOCATION}}
// Microsoft Developer Studio will insert additional declarations immediately before the previous line.


OPENMPT_NAMESPACE_END