/////////////////////////////////////////////////////////////////////////////
// Name:        Cache.cpp
// Purpose:     Manage cache of transcoded files
// Author:      Alex Thuering
// Created:     29.08.2008
// RCS-ID:      $Id: Cache.cpp,v 1.22 2015/01/31 13:35:05 ntalex Exp $
// Copyright:   (c) Alex Thuering
// Licence:     GPL
/////////////////////////////////////////////////////////////////////////////

#include "Cache.h"
#include "Config.h"
#include "MessageDlg.h"
#include <wx/log.h>
#include <wx/dir.h>
#include <wx/file.h>
#include <wx/tokenzr.h>

const wxString CACHE_SUB_DIR = wxT("dvd-cache");

/**
 * Stores cache entry information
 */
class CacheEntry {
public:
	CacheEntry(int idx);
	CacheEntry(Vob* vob, DVD* dvd, int idx);
	bool equals(const CacheEntry& entry);
	inline int GetIdx() { return m_idx; }
	inline wxString GetVideoFile() { return m_videoFile; }
	inline bool IsUsed() { return m_used; }
	inline void SetUsed(bool used) { m_used = used; }
	wxSvgXmlNode* GetXML();
	bool PutXML(wxSvgXmlNode* node);
	
private:
	wxString m_videoFile;
	wxArrayString m_audioFiles;
	wxArrayString m_subtitleFiles;
	wxArrayInt m_formats;
    int m_videoBitrate;
    int m_audioBitrate;
	double m_startTime;
	double m_recordingTime;
	wxString m_videoFilters;
	wxString m_audioFilters;
    int m_idx;
    bool m_used;
};

CacheEntry::CacheEntry(int idx) {
	m_used = false;
	m_idx = idx;
	m_videoBitrate = 0;
	m_audioBitrate = 0;
	m_startTime = 0;
	m_recordingTime = -1;
}

CacheEntry::CacheEntry(Vob* vob, DVD* dvd, int idx) {
	m_videoFile = vob->GetFilename();
	m_audioFiles = vob->GetAudioFilenames();
	for (unsigned int i = 0; i < vob->GetSubtitles().GetCount(); i++)
		m_subtitleFiles.Add(vob->GetSubtitles()[i]->AsString());
	for (unsigned int stIdx = 0; stIdx < vob->GetStreams().size(); stIdx++)
		m_formats.Add(vob->GetStreams()[stIdx]->GetDestinationFormat());
	m_videoBitrate = dvd->GetVideoBitrate();
	m_audioBitrate = dvd->GetAudioBitrate();
	m_startTime = vob->GetStartTime();
	m_recordingTime = vob->GetRecordingTime();
	m_videoFilters = vob->GetAllVideoFilters();
	for (unsigned int stIdx = 0; stIdx < vob->GetStreams().size(); stIdx++) {
		if (vob->GetStreams()[stIdx]->GetType() == stAUDIO) {
			m_audioFilters += wxT('|') + vob->GetStreams()[stIdx]->GetAllAudioFilters(true);
		}
	}
	m_idx = idx;
	m_used = false;
}

bool CacheEntry::equals(const CacheEntry& entry) {
	if (m_videoFile != entry.m_videoFile
			|| m_audioFiles != entry.m_audioFiles
			|| m_subtitleFiles != entry.m_subtitleFiles
			|| m_videoBitrate != entry.m_videoBitrate
			|| m_audioBitrate != entry.m_audioBitrate
			|| m_startTime != entry.m_startTime
			|| m_recordingTime != entry.m_recordingTime
			|| m_videoFilters != entry.m_videoFilters
			|| m_audioFilters != entry.m_audioFilters
			|| m_formats.Count() != entry.m_formats.Count())
		return false;
	for (unsigned int i = 0; i < m_formats.GetCount(); i++)
		if (m_formats[i] != entry.m_formats[i])
			return false;
	return true;
}

wxSvgXmlNode* CacheEntry::GetXML() {
	wxSvgXmlNode* entryNode = new wxSvgXmlNode(wxSVGXML_ELEMENT_NODE, wxT("entry"));
	entryNode->AddProperty(wxT("videoFile"), m_videoFile);
	if (m_audioFiles.size()) {
		wxString audioFiles;
		for (unsigned int i = 0; i < m_audioFiles.size(); i++)
			audioFiles += wxT('|') + m_audioFiles[i];
		entryNode->AddProperty(wxT("audioFiles"), audioFiles.substr(1));
	}
	if (m_subtitleFiles.size()) {
		wxString subtitleFiles;
		for (unsigned int i = 0; i < m_subtitleFiles.size(); i++)
			subtitleFiles += wxT('|') + m_subtitleFiles[i];
		entryNode->AddProperty(wxT("subtitleFiles"), subtitleFiles.substr(1));
	}
	wxString formats;
	for (unsigned int i = 0; i < m_formats.size(); i++)
		formats += wxT('|') + wxString::Format(wxT("%d"), m_formats[i]);
	entryNode->AddProperty(wxT("formats"), formats.substr(1));
	entryNode->AddProperty(wxT("videoBitrate"), wxString::Format(wxT("%d"), m_videoBitrate));
	entryNode->AddProperty(wxT("audioBitrate"), wxString::Format(wxT("%d"), m_audioBitrate));
	if (m_startTime > 0)
		entryNode->AddProperty(wxT("startTime"), wxString::Format(wxT("%g"), m_startTime));
	if (m_recordingTime != -1)
		entryNode->AddProperty(wxT("recordingTime"), wxString::Format(wxT("%g"), m_recordingTime));
	if (m_videoFilters.length() > 0)
		entryNode->AddProperty(wxT("videoFilters"), m_videoFilters);
	if (m_audioFilters.length() > 0)
		entryNode->AddProperty(wxT("audioFilters"), m_audioFilters);
	return entryNode;
}

bool CacheEntry::PutXML(wxSvgXmlNode* node) {
	wxString val;
	node->GetPropVal(wxT("videoFile"), &m_videoFile);
	node->GetPropVal(wxT("audioFiles"), &val);
	if (val.length()) {
		wxStringTokenizer tokenizer(val, wxT("|"));
		while (tokenizer.HasMoreTokens()) {
			wxString token = tokenizer.GetNextToken();
			m_audioFiles.Add(token);
		}
	}
	node->GetPropVal(wxT("subtitleFiles"), &val);
	if (val.length()) {
		wxStringTokenizer tokenizer(val, wxT("|"));
		while (tokenizer.HasMoreTokens()) {
			wxString token = tokenizer.GetNextToken();
			m_subtitleFiles.Add(token);
		}
	}
	node->GetPropVal(wxT("formats"), &val);
	if (val.length()) {
		wxStringTokenizer tokenizer(val, wxT("|"));
		while (tokenizer.HasMoreTokens()) {
			long lval;
			if (tokenizer.GetNextToken().ToLong(&lval)) {
				m_formats.Add((int) lval);
			}
		}
	}
	long lval;
	if (node->GetPropVal(wxT("videoBitrate"), &val) && val.ToLong(&lval)) {
		m_videoBitrate = (int) lval;
	}
	if (node->GetPropVal(wxT("audioBitrate"), &val) && val.ToLong(&lval)) {
		m_audioBitrate = (int) lval;
	}
	double dval;
	if (node->GetPropVal(wxT("startTime"), &val) && val.ToDouble(&dval)) {
		m_startTime = lval;
	}
	if (node->GetPropVal(wxT("recordingTime"), &val) && val.ToDouble(&dval)) {
		m_recordingTime = lval;
	}
	node->GetPropVal(wxT("videoFilters"), &m_videoFilters);
	node->GetPropVal(wxT("audioFilters"), &m_audioFilters);

	return true;
}

Cache::Cache() {
	// noting to do
}

Cache::~Cache() {
	for (CacheMap::iterator it = m_cacheMap.begin(); it != m_cacheMap.end(); it++)
		delete it->second;
	m_cacheMap.clear();
}

/**
 * Returns true if the cache is already initialized
 */
bool Cache::IsInitialized() {
	return m_dir.length() > 0;
}

bool Cache::SetTempDir(wxString tempDir) {
	// check if temp dir exist 
	if (!wxDir::Exists(tempDir) && !wxMkdir(tempDir)) {
		wxLogError(_T("Can't create directory '%s'"), tempDir.c_str());
		return false;
	}
	// set new cache directory
	wxString cacheDir = tempDir + wxFILE_SEP_PATH + CACHE_SUB_DIR;
	if (!wxDir::Exists(cacheDir) && !wxMkdir(cacheDir)) {
		wxLogError(_T("Can't create directory '%s'"), cacheDir.c_str());
		return false;
	}
	// clear cache map if cache directory is changed
	if (m_dir.length() > 0 && m_dir != cacheDir) {
		Clear();
	}
	bool doOpen = m_dir != cacheDir;
	m_dir = cacheDir;
	if (doOpen) {
		Open();
	}
	return true;
}

/**
 * Returns temporally directory
 */
wxString Cache::GetTempDir() {
	if (!IsInitialized())
		return s_config.GetTempDir(); // default temporally directory
	return m_dir.BeforeLast(wxFILE_SEP_PATH); // temporally directory without CACHE_SUB_DIR
}

int Cache::GetCount() {
	return m_cacheMap.size();
}

/**
 * Return size of files in cache (in KB)
 */
long Cache::GetSize() {
	long size = 0;
	for (CacheMap::iterator it = m_cacheMap.begin(); it != m_cacheMap.end(); it++) {
		CacheEntry* entry = it->second;
		wxString filename = GetFileName(entry->GetIdx());
		if (wxFile::Exists(filename)) {
			size += wxFile(filename).Length()/1024;
		}
	}
	return size;
}

wxString Cache::Find(Vob* vob, DVD* dvd, bool removeOld) {
	CacheMap::iterator it = m_cacheMap.find(vob->GetFilename());
	if (it == m_cacheMap.end())
		return wxT(""); // no entry for given source file exists

	// check if cache entry match
	CacheEntry* entry = it->second;
	wxString filename = GetFileName(entry->GetIdx());
	if (entry->equals(CacheEntry(vob, dvd, -1)) && wxFile::Exists(filename)) {
		entry->SetUsed(true);
		return filename;
	}
	// remove old entry
	if (removeOld) {
		if (wxFileExists(filename) && !wxRemoveFile(filename)) {
			wxLogError(wxString::Format(_("Can't remove file '%s'"), filename.c_str()));
		}
		m_cacheMap.erase(it->first);
		delete entry;
	}
	return wxT("");
}

wxString Cache::Add(Vob* vob, DVD* dvd) {
	int idx = GenerateIndex();
	CacheEntry* entry = new CacheEntry(vob, dvd, idx);
	m_cacheMap[vob->GetFilename()] = entry;
	wxString filename = GetFileName(idx);
	if (wxFileExists(filename) && !wxRemoveFile(filename)) {
		wxLogError(wxString::Format(_("Can't remove file '%s'"), filename.c_str()));
	}
	return filename;
}

void Cache::Remove(Vob* vob) {
	if (wxFileExists(vob->GetTmpFilename()))
		wxRemoveFile(vob->GetTmpFilename());
	m_cacheMap.erase(m_cacheMap.find(vob->GetFilename()));
}

void Cache::Clear() {
	if (!IsInitialized())
		return;
	for (CacheMap::iterator it = m_cacheMap.begin(); it != m_cacheMap.end(); it++)
		delete it->second;
	m_cacheMap.clear();
	// remove files
	if (!wxDirExists(m_dir))
		return;
	wxDir d(m_dir);
	wxString fname;
	while (d.GetFirst(&fname, wxEmptyString, wxDIR_FILES)) {
		if (!wxRemoveFile(m_dir + wxFILE_SEP_PATH + fname)) {
			wxLogError(wxString::Format(_("Can't remove file '%s'"), fname.c_str()));
		}
	}
	d.Open(wxGetHomeDir());
	wxRmdir(m_dir);
}

void Cache::BeginClean() {
	for (CacheMap::iterator it = m_cacheMap.begin(); it != m_cacheMap.end(); it++)
		it->second->SetUsed(false);
}

void Cache::EndClean() {
	CacheMap::iterator it = m_cacheMap.begin();
	while (it != m_cacheMap.end()) {
		CacheEntry* entry = it->second;
		if (!entry->IsUsed()) {
			if (wxFileExists(GetFileName(entry->GetIdx())))
				wxRemoveFile(GetFileName(entry->GetIdx()));
			m_cacheMap.erase(it++);
			delete entry;
		} else
			++it;
	}
}

wxString Cache::GetFileName(int index) {
	return m_dir + wxFILE_SEP_PATH + wxString::Format(wxT("entry%03d.vob"), index);
}

int Cache::GenerateIndex() {
	int idx = 0;
	for (CacheMap::iterator it = m_cacheMap.begin(); it != m_cacheMap.end(); it++) {
		if (it->second->GetIdx() > idx)
			idx = it->second->GetIdx();
	}
	return idx + 1;
}

/**
 * Loads list of cache entries
 */
bool Cache::Open() {
	wxString cacheFile =  m_dir + wxFILE_SEP_PATH + wxT("cache.xml");
	if (!wxFileExists(cacheFile))
		return false;
	
	wxSvgXmlDocument xml;
	if (!xml.Load(cacheFile)) {
		return false;
	}

	wxSvgXmlNode* root = xml.GetRoot();
	if (root == NULL || root->GetName() != wxT("cache")) {
		return false;
	}
	
	wxSvgXmlNode* child = root->GetChildren();
	while (child) {
		if (child->GetName() == wxT("entry")) {
			int idx = GenerateIndex();
			CacheEntry* entry = new CacheEntry(idx);
			if (entry->PutXML(child)) {
				m_cacheMap[entry->GetVideoFile()] = entry;
			}
		}
		child = child->GetNext();
	}
	return true;
}

/**
 * Saves list of cache entries
 */
void Cache::Save() {
	wxString cacheFile =  m_dir + wxFILE_SEP_PATH + wxT("cache.xml");
	if (m_cacheMap.size() == 0) {
		if (wxFileExists(cacheFile)) {
			wxRemoveFile(cacheFile);
		}
		return;
	}
	wxSvgXmlDocument* xml = new wxSvgXmlDocument;
	wxSvgXmlNode* root = new wxSvgXmlNode(wxSVGXML_ELEMENT_NODE, wxT("cache"));
	for (CacheMap::iterator it = m_cacheMap.begin(); it != m_cacheMap.end(); it++) {
		root->AddChild(it->second->GetXML());
	}
	xml->SetRoot(root);
	xml->Save(cacheFile);
	delete xml;
}

/**
 * Shows prompt dialog and clears or saves cache 
 */
void Cache::ShowClearPrompt(wxWindow* parent) {
	// check entries
	for (CacheMap::iterator it = m_cacheMap.begin(); it != m_cacheMap.end(); it++) {
		CacheEntry* entry = it->second;
		wxString filename = GetFileName(entry->GetIdx());
		if (!wxFile::Exists(filename)) {
			m_cacheMap.erase(it);
		}
	}
	if (GetCount() == 0) {
		Save();
		return;
	}
	bool clear = s_config.GetCacheClearPrompt() == 1;
	if (s_config.GetCacheClearPrompt() == 0) {
		wxString msg = wxPLURAL("There is %d file (%0.1f MB) in the transcoding cache.",
				"There are %d files (%0.1f MB) in the transcoding cache.", GetCount());
		msg = wxString::Format(msg, GetCount(), ((float) GetSize()) / 1024) + wxT("\n");
		msg += _("Do you want to clear the cache?");
		MessageDlg msgDlg(parent, msg, _("Confirm"), wxYES_NO|wxICON_QUESTION);
		clear = msgDlg.ShowModal() == wxYES;
		s_config.SetCacheClearPrompt(msgDlg.IsShowAgain() ? 0 : (clear ? 1 : 2));
	}
	if (clear) {
		Clear();
	} else {
		Save();
	}
}
