/* vi: set sw=4 ts=4:
 *
 * Copyright (C) 2006 - 2015 Christian Hohnstaedt.
 *
 * All rights reserved.
 */

#include <QAbstractItemModel>
#include <QAbstractItemView>
#include <QContextMenuEvent>
#include <QMenu>
#include <QVariant>
#include <QRegularExpression>
#include <QFileDialog>
#include <QClipboard>
#include <QActionGroup>

#include "OidResolver.h"
#include "XcaHeaderView.h"
#include "XcaTreeView.h"
#include "XcaProxyModel.h"
#include "MainWindow.h"
#include "XcaWarning.h"
#include "XcaDialog.h"
#include "XcaApplication.h"
#include "ExportDialog.h"
#include "ImportMulti.h"
#include "lib/load_obj.h"
#include "lib/pass_info.h"
#include "lib/PwDialogCore.h"

#include "ui_ItemProperties.h"

XcaTreeView::XcaTreeView(QWidget *parent)
	:QTreeView(parent)
{
	setHeader(new XcaHeaderView());
	setAlternatingRowColors(true);
	setSelectionMode(QAbstractItemView::ExtendedSelection);
	setEditTriggers(QAbstractItemView::EditKeyPressed);
	setRootIsDecorated(false);
	setUniformRowHeights(true);
	setDragEnabled(true);
	//setAnimated(true);

	proxy = new XcaProxyModel(this);
	proxy->setDynamicSortFilter(true);
	sortByColumn(0, Qt::AscendingOrder);
	connect(header(), SIGNAL(sectionHandleDoubleClicked(int)),
		this, SLOT(resizeColumnToContents(int)));
	connect(this, SIGNAL(doubleClicked(const QModelIndex &)),
		this, SLOT(doubleClick(const QModelIndex &)));
	header()->setSectionsClickable(true);
	throttle.setSingleShot(true);
	connect(&throttle, SIGNAL(timeout()), this, SLOT(columnsResize()));
	connect(&throttle, SIGNAL(timeout()), proxy, SLOT(invalidate()));
	setFocusPolicy(Qt::StrongFocus);
	setExpandsOnDoubleClick(false);

	setFont(XcaApplication::tableFont);
}

XcaTreeView::~XcaTreeView()
{
	delete proxy;
}

void XcaTreeView::contextMenuEvent(QContextMenuEvent * e)
{
	QModelIndex index;
	if (!basemodel)
		return;
	index = indexAt(e->pos());
	showContextMenu(e, getIndex(index));
}

void XcaTreeView::showHideSections()
{
	if (!basemodel)
		return;
	int i, max = basemodel->columnCount(QModelIndex());
	basemodel->colResizeStart();
	for (i=0; i<max; i++) {
		if (basemodel->columnHidden(i))
			header()->hideSection(i);
		else
			header()->showSection(i);
	}
	basemodel->colResizeEnd();
	columnsResize();
}

void XcaTreeView::setMainwin(MainWindow *mw, QLineEdit *filter)
{
	mainwin = mw;
	connect(filter, SIGNAL(textChanged(const QString &)),
		this, SLOT(setFilter(const QString&)));
}

void XcaTreeView::setModel(QAbstractItemModel *model)
{
	QByteArray ba;

	header()->setStretchLastSection(false);
	setSortingEnabled(false);

	basemodel = dynamic_cast<db_base*>(model);
	proxy->setSourceModel(model);
	QTreeView::setModel(model ? proxy : nullptr);

	if (basemodel) {
		setRootIsDecorated(basemodel->treeViewMode());
		connect(basemodel, SIGNAL(resetHeader()),
			header(), SLOT(resetMoves()));
		connect(basemodel, SIGNAL(resetHeader()),
			this, SLOT(columnsResize()));
		connect(header(), SIGNAL(sectionMoved(int,int,int)),
			this, SLOT(sectionMoved(int,int,int)));
		connect(header(), SIGNAL(sectionResized(int,int,int)),
			basemodel, SLOT(sectionResized(int,int,int)));
		connect(header(), SIGNAL(sortIndicatorChanged(int,Qt::SortOrder)),
			basemodel, SLOT(sortIndicatorChanged(int,Qt::SortOrder)));
		connect(basemodel, SIGNAL(columnsContentChanged()),
			this, SLOT(columnsChanged()));
		connect(selectionModel(), SIGNAL(currentChanged(const QModelIndex &,
										const QModelIndex &)),
			this, SLOT(itemSelectionChanged(const QModelIndex &,
										const QModelIndex &)));

		basemodel->initHeaderView(header());
		setSortingEnabled(true);
		header()->setStretchLastSection(true);

	}
	showHideSections();
}

void XcaTreeView::headerEvent(QContextMenuEvent *e, int col)
{
	contextMenu(e, NULL, col);
}

QModelIndex XcaTreeView::getIndex(const QModelIndex &index)
{
	return proxy->mapToSource(index);
}

QModelIndex XcaTreeView::getProxyIndex(const QModelIndex &index)
{
	return proxy->mapFromSource(index);
}

void XcaTreeView::itemSelectionChanged(const QModelIndex &m, const QModelIndex &)
{
	QModelIndex index = getIndex(m);
	QVariant v;
	qDebug() << "selectionChanged()" << index.isValid() << index.row() << index.column();
	if (m.isValid()) {
		pki_base *pki = db_base::fromIndex(index);
		if (pki)
			v = pki->getSqlItemId();
	}
	if (basemodel)
		basemodel->setSelected(v);
}

QModelIndexList XcaTreeView::getSelectedIndexes()
{
	QModelIndexList list;
	QItemSelection indexes = selectionModel()->selection();
	list = proxy->mapSelectionToSource(indexes).indexes();

	/* Reduce list to column 0 items */
	QModelIndexList::iterator it = list.begin();
	while (it != list.end()) {
		if ((*it).column() != 0)
			it = list.erase(it);
		else
			++it;
	}
	return list;
}

void XcaTreeView::columnsChanged()
{
	throttle.start(200);
}

void XcaTreeView::columnsResize()
{
	int cnt, i;
	if (!basemodel)
		return;
	cnt = basemodel->columnCount(QModelIndex());
	basemodel->colResizeStart();
	for (i=0; i<cnt; i++) {
		if (!basemodel->fixedHeaderSize(i)) {
			resizeColumnToContents(i);
		}
	}
	basemodel->colResizeEnd();
}

void XcaTreeView::sectionMoved(int, int, int)
{
	int cnt = header()->count();
	for (int i=0; i<cnt; i++) {
		basemodel->setVisualIndex(i, header()->visualIndex(i));
	}
}

QModelIndex XcaTreeView::currentIndex()
{
	QModelIndex idx = QTreeView::currentIndex();
	idx = getIndex(idx);
	idx = basemodel->index(idx.row(), 0, idx.parent());
	if (!idx.isValid()) {
		QModelIndexList l = getSelectedIndexes();
		if (l.size() > 0)
			idx = l[0];
	}
	return idx;
}

void XcaTreeView::editIdx()
{
	edit(getProxyIndex(currentIndex()));
}

void XcaTreeView::setFilter(const QString &pattern)
{
	pki_base::limitPattern = QRegularExpression(pattern,
			QRegularExpression::CaseInsensitiveOption);
	// Only to tell the model about the changed filter
	proxy->setFilterFixedString(pattern);
}

void XcaTreeView::deleteItems()
{
	QModelIndex index;
	QModelIndexList indexes = getSelectedIndexes();
	QStringList items;
	pki_base *pki = NULL;

	if (indexes.count() == 0 || !basemodel)
		return;

	foreach(index, indexes) {
		pki = db_base::fromIndex(index);
		if (pki)
			items << pki->getIntName();
	}

	Transaction;
	if (!TransBegin())
		return;

	if (!XCA_OKCANCEL(pki->getMsg(pki_base::msg_delete,
			items.size()).arg(items.join("', '"))))
		return;

	foreach(index, indexes) {
		basemodel->deletePKI(index);
	}
	TransCommit();
}

void XcaTreeView::exportItems()
{
	exportItems(getSelectedIndexes());
}

void XcaTreeView::showItems()
{
	QModelIndexList indexes = getSelectedIndexes();
	foreach(QModelIndex index, indexes)
		showItem(index);
}

void XcaTreeView::newItem()
{
	if (basemodel)
		basemodel->newItem();
}

void XcaTreeView::load_default(load_base *load)
{
	QString s;
	QStringList slist = QFileDialog::getOpenFileNames(NULL, load->caption,
				Settings["workingdir"], load->filter);

	if (!slist.count())
		return;

	update_workingdir(slist[0]);

	ImportMulti *dlgi = new ImportMulti(NULL);
	foreach(s, slist) {
		pki_base *item = NULL;
		try {
			item = load->loadItem(s);
			dlgi->addItem(item);
		}
		catch (errorEx &err) {
			XCA_ERROR(err);
			delete item;
		}
	}
	dlgi->execute();
	delete dlgi;
}

void XcaTreeView::doubleClick(const QModelIndex &m)
{
	showItem(getIndex(m));
}

void XcaTreeView::editComment()
{
	pki_base *item = db_base::fromIndex(currentIndex());
	if (!basemodel || !item)
		return;

	QWidget *w = new QWidget(nullptr);
	Ui::ItemProperties *prop = new Ui::ItemProperties();
	prop->setupUi(w);
	prop->comment->setPlainText(item->getComment());
	prop->name->setText(item->getIntName());
	prop->source->setText(item->pki_source_name());
	prop->insertionDate->setText(item->getInsertionDate().toPretty());
	XcaDialog *d = new XcaDialog(this, item->getType(), w,
		tr("Item properties"), QString(), "itemproperties");
	if (d->exec()) {
		item->setIntName(prop->name->text());
		item->setComment(prop->comment->toPlainText());
		basemodel->updateItem(item);
	}
	delete d;
}

void XcaTreeView::pem2clipboard()
{
	if (basemodel) try {
		QString msg = basemodel->pem2QString(getSelectedIndexes());
		QClipboard *cb = QApplication::clipboard();

		if (cb->supportsSelection())
			cb->setText(msg, QClipboard::Selection);
		cb->setText(msg);
	} catch (errorEx &err) {
		XCA_ERROR(err);
	}
}

void XcaTreeView::headerDetails()
{
	if (curr_hd && curr_hd->id > 0 && mainwin)
		mainwin->getResolver()->searchOid(QString::number(curr_hd->id));
}

void XcaTreeView::columnRemove(void)
{
	if (curr_hd->action)
		curr_hd->action->setChecked(false);
}

void XcaTreeView::showItem(const QModelIndex &index)
{
	pki_base *pki = db_base::fromIndex(index);
	showItem(pki);
}

void XcaTreeView::showItem(const QString &name)
{
	pki_base *pki = Store.lookupPki<pki_base>(name.toULongLong());
	showItem(pki);
}

void XcaTreeView::showItem(pki_base *pki)
{
	if (pki && pki->isVisible() == 1)
		showPki(pki);
}

static void addSubmenu(tipMenu *menu, tipMenu *sub)
{
	if (sub->isEmpty())
		delete sub;
	else
		menu->addMenu(sub);
}

void XcaTreeView::contextMenu(QContextMenuEvent *e, QMenu *parent, int col)
{
	int shown = 0;
	tipMenu *menu, *dn, *v3ext, *current, *v3ns, *keyprop;
	QAction *a;
	dbheader *hd;
	dbheaderList allHeaders = basemodel->getAllHeaders();

	menu = new tipMenu(QString(), mainwin);
	dn = new tipMenu(tr("Subject entries"), mainwin);
	v3ext = new tipMenu(tr("X509v3 Extensions"), mainwin);
	v3ns = new tipMenu(tr("Netscape extensions"), mainwin);
	keyprop = new tipMenu(tr("Key properties"), mainwin);
	menu->addAction(tr("Reset"), basemodel, SLOT(columnResetDefaults()));
	if (col >= 0 && col < allHeaders.size()) {
		curr_hd = allHeaders[col];
		menu->addAction(tr("Hide Column"), this,SLOT(columnRemove()));
		if (curr_hd->id > 0)
			menu->addAction(tr("Details"), this,
						SLOT(headerDetails()));
	}
	menu->addSeparator();
	foreach(hd, allHeaders) {
		switch (hd->type) {
			case dbheader::hd_x509name:
				current = dn;
				break;
			case dbheader::hd_v3ext:
				current = v3ext;
				break;
			case dbheader::hd_v3ext_ns:
				if (Settings["disable_netscape"])
					continue;
				current = v3ns;
				break;
			case dbheader::hd_key:
				current = keyprop;
				break;
			default:
				current = menu;
				break;
		}
		a = current->addAction(hd->getName());
		a->setCheckable(true);
		a->setChecked(hd->show);
		a->setToolTip(hd->getTooltip());
		hd->action = a;
	}

	menu->addSeparator();

	addSubmenu(menu, dn);
	addSubmenu(v3ext, v3ns);
	addSubmenu(menu, v3ext);
	addSubmenu(menu, keyprop);


	if (parent) {
		parent->addSeparator();
		parent->addMenu(menu)->setText(tr("Columns"));
		parent->exec(e->globalPos());
	} else {
		menu->exec(e->globalPos());
	}
	foreach(hd, allHeaders) {
		if (hd->action)
			hd->show = hd->action->isChecked();
		shown += hd->show ? 1 : 0;
		hd->action = NULL;
	}
	if (!shown)
		allHeaders[0]->show = true;
	delete menu;
	delete parent;
	showHideSections();
}

void XcaTreeView::changeView()
{
	if (!basemodel)
		return;
	hide();
	basemodel->changeView();
	show();
}

void XcaTreeView::exportItems(const QModelIndexList &indexes,
	const QString &filename, const pki_export *xport)
{
	XFile file(filename);

	if (xport->match_all(F_PRIVATE))
		file.open_key();
	else
		file.open_write();

	basemodel->exportItems(indexes, xport, file);
}

void XcaTreeView::exportItems(const QModelIndexList &indexes)
{
	if (!basemodel || indexes.empty())
		return;

	ExportDialog *dlg = exportDialog(indexes);
	if (dlg && dlg->exec()) {
		try {
			QString fname = dlg->filename->text();
			const pki_export *xport = dlg->export_type();

			if (dlg->separateFiles->isChecked()) {
				Passwd pass;
				if (xport->match_all(F_CRYPT) && dlg->samePassword->isChecked())
				{
					// Plural form not required for < 2 items
					// Will only be called for 2 or more items
					pass_info p(tr("Export Password"),
							tr("Please enter the password to encrypt all %n "
							   "exported private key(s) in:\n%1",
							   "Singular form for 0 or 1 item can be ignored. Will always be called with n >= 2",
									indexes.size()).arg(fname),
							this);

					// Ask for an encryption password once
					if (PwDialogCore::execute(&p, &pass, true) != 1)
						return;
				}
				for (QModelIndex idx : indexes) {
					pki_base *pki = db_base::fromIndex(idx);
					// Will be cleared by PwDialogCore
					PwDialogCore::cmdline_passwd = pass;
					QString fn = pki->get_dump_filename(fname,
							QString(".%1").arg(xport->extension));
					exportItems(QModelIndexList() << idx, fn, xport);
				}
				PwDialogCore::cmdline_passwd.cleanse();
			} else {
				exportItems(indexes, fname, xport);
			}
		} catch (errorEx &err) {
			XCA_ERROR(err);
		}
	}
	delete dlg;
}

ExportDialog *XcaTreeView::exportDialog(const QModelIndexList &)
{
	return nullptr;
}

void XcaTreeView::showContextMenu(QContextMenuEvent *e,
		const QModelIndex &idx)
{
	QMenu *menu = new QMenu(mainwin);
	QMenu *subExport = NULL;
	QModelIndexList indexes = getSelectedIndexes();
	QModelIndex index;

	index = idx.isValid() ? idx : currentIndex();
	menu->addAction(tr("New"), this, SLOT(newItem()));
	menu->addAction(tr("Import"), this, SLOT(load()));
	menu->addAction(tr("Paste PEM data"), mainwin, SLOT(pastePem()))->
			setShortcut(QKeySequence::Paste);

	if (indexes.size() == 1) {
		menu->addAction(tr("Rename"), this, SLOT(editIdx()));
		menu->addAction(tr("Properties"), this, SLOT(editComment()));
	}
	if (indexes.size() > 0) {
		menu->addAction(tr("Delete"), this, SLOT(deleteItems()))->
				setShortcut(QKeySequence::Delete);
		subExport = menu->addMenu(tr("Export"));
		const pki_export *xport = pki_export::by_id(Settings[ClipboardSettings]);
		QString clipboard = tr("Clipboard");
		if (xport)
			clipboard = QString("%1 (%2)").arg(clipboard).arg(xport->desc);

		subExport->addAction(clipboard, this,
				SLOT(pem2clipboard()))->setShortcut(QKeySequence::Copy);
		subExport->addAction(tr("File"), this, SLOT(exportItems()))->
				setShortcut(QKeySequence::Save);
	}

	clipboardFormatMenu(menu);

	fillContextMenu(menu, subExport, index, indexes);

	contextMenu(e, menu, -1);
}

void XcaTreeView::keyPressEvent(QKeyEvent *event)
{
	switch (event->key()) {
		case Qt::Key_Backspace:
		case Qt::Key_Delete:
			deleteItems();
			return;
		case Qt::Key_Enter:
		case Qt::Key_Return:
			if (state() != QAbstractItemView::EditingState)
				showItems();
			return;
		case Qt::Key_F2:
			editIdx();
			return;
		case Qt::Key_Escape:
			clearSelection();
			return;
	}
	if (event->matches(QKeySequence::Save)) {
		exportItems();
		return;
	}
	if (event->matches(QKeySequence::Copy)) {
		pem2clipboard();
		return;
	}
	QTreeView::keyPressEvent(event);
}

void XcaTreeView::changeEvent(QEvent *event)
{
    switch (event->type()) {
		case QEvent::StyleChange:
		case QEvent::PaletteChange:
			qDebug() << "Style change event" << event->type();
			pki_base::setupColors(palette());
			break;
		default:
			break;
	}
    QTreeView::changeEvent(event);
}

void XcaTreeView::clipboardFormatMenu(QMenu *menu)
{
	if (!ClipboardSettings || !ClipboardPki_type)
		return;
	int exp_type = Settings[ClipboardSettings];

	QActionGroup *group = new QActionGroup(menu);
	QMenu *clipboard = menu->addMenu(tr("Clipboard format"));
	foreach(const pki_export *x, pki_export::select(ClipboardPki_type, 0)) {
		if (!(x->flags & F_CLIPBOARD))
			continue;
		QAction *a = clipboard->addAction(x->desc);
		a->setData(x->id);
		a->setCheckable(true);
		a->setChecked(exp_type == x->id);
		group->addAction(a);
	}

	connect(group, SIGNAL(triggered(QAction*)),
		this, SLOT(clipboardFormat(QAction*)));
}

void XcaTreeView::clipboardFormat(QAction *a)
{
	if (ClipboardSettings)
		Settings[ClipboardSettings] = a->data().toInt();
}
