/*
 * Copyright 2007-2015, Axel Dörfler, axeld@pinc-software.de.
 * Distributed under the terms of the MIT License.
 */


#include "SudokuWindow.h"

#include <stdio.h>

#include <Alert.h>
#include <Application.h>
#include <Catalog.h>
#include <File.h>
#include <FilePanel.h>
#include <FindDirectory.h>
#include <LayoutBuilder.h>
#include <Menu.h>
#include <MenuBar.h>
#include <MenuItem.h>
#include <Path.h>
#include <Roster.h>

#include <be_apps/Tracker/RecentItems.h>

#include "ProgressWindow.h"
#include "Sudoku.h"
#include "SudokuField.h"
#include "SudokuGenerator.h"
#include "SudokuView.h"


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "SudokuWindow"


const uint32 kMsgOpenFilePanel = 'opfp';
const uint32 kMsgGenerateSudoku = 'gnsu';
const uint32 kMsgAbortSudokuGenerator = 'asgn';
const uint32 kMsgSudokuGenerated = 'sugn';
const uint32 kMsgMarkInvalid = 'minv';
const uint32 kMsgMarkValidHints = 'mvht';
const uint32 kMsgStoreState = 'stst';
const uint32 kMsgRestoreState = 'rest';
const uint32 kMsgNewBlank = 'new ';
const uint32 kMsgStartAgain = 'stag';
const uint32 kMsgExportAs = 'expt';


enum sudoku_level {
	kEasyLevel		= 0,
	kAdvancedLevel	= 2,
	kHardLevel		= 4,
};


class GenerateSudoku {
public:
								GenerateSudoku(SudokuField& field, int32 level,
									BMessenger progress, BMessenger target);
								~GenerateSudoku();

			void				Abort();

private:
			void				_Generate();
	static	status_t			_GenerateThread(void* self);

			SudokuField			fField;
			BMessenger			fTarget;
			BMessenger			fProgress;
			thread_id			fThread;
			int32				fLevel;
			bool				fQuit;
};


GenerateSudoku::GenerateSudoku(SudokuField& field, int32 level,
		BMessenger progress, BMessenger target)
	:
	fField(field),
	fTarget(target),
	fProgress(progress),
	fLevel(level),
	fQuit(false)
{
	fThread = spawn_thread(_GenerateThread, "sudoku generator",
		B_LOW_PRIORITY, this);
	if (fThread >= B_OK)
		resume_thread(fThread);
	else
		_Generate();
}


GenerateSudoku::~GenerateSudoku()
{
	Abort();
}


void
GenerateSudoku::Abort()
{
	fQuit = true;

	status_t status;
	wait_for_thread(fThread, &status);
}


void
GenerateSudoku::_Generate()
{
	SudokuGenerator generator;

	bigtime_t start = system_time();
	generator.Generate(&fField, 40 - fLevel * 5, fProgress, &fQuit);
	printf("generated in %g msecs\n", (system_time() - start) / 1000.0);

	BMessage done(kMsgSudokuGenerated);
	if (!fQuit) {
		BMessage field;
		if (fField.Archive(&field, true) == B_OK)
			done.AddMessage("field", &field);
	}

	fTarget.SendMessage(&done);
}


/*static*/ status_t
GenerateSudoku::_GenerateThread(void* _self)
{
	GenerateSudoku* self = (GenerateSudoku*)_self;
	self->_Generate();
	return B_OK;
}


//	#pragma mark -


SudokuWindow::SudokuWindow()
	:
	BWindow(BRect(-1, -1, 400, 420), B_TRANSLATE_SYSTEM_NAME("Sudoku"),
		B_TITLED_WINDOW, B_ASYNCHRONOUS_CONTROLS | B_QUIT_ON_WINDOW_CLOSE),
	fGenerator(NULL),
	fStoredState(NULL),
	fExportFormat(kExportAsText)
{
	BMessage settings;
	_LoadSettings(settings);

	BRect frame;
	if (settings.FindRect("window frame", &frame) == B_OK) {
		MoveTo(frame.LeftTop());
		ResizeTo(frame.Width(), frame.Height());
		frame.OffsetTo(B_ORIGIN);
	} else {
		float scaling = std::max(1.0f, be_plain_font->Size() / 12.0f);
		ResizeTo(Frame().Width() * scaling, Frame().Height() * scaling);
		frame = Bounds();
	}

	MoveOnScreen();

	if (settings.HasMessage("stored state")) {
		fStoredState = new BMessage;
		if (settings.FindMessage("stored state", fStoredState) != B_OK) {
			delete fStoredState;
			fStoredState = NULL;
		}
	}

	int32 level = 0;
	settings.FindInt32("level", &level);

	// Create GUI

	BMenuBar* menuBar = new BMenuBar("menu");
	fSudokuView = new SudokuView("sudoku view", settings);

	BLayoutBuilder::Group<>(this, B_VERTICAL, 0)
		.Add(menuBar)
		.Add(fSudokuView);

	// Build menu

	// "File" menu
	BMenu* menu = new BMenu(B_TRANSLATE("File"));
	fNewMenu = new BMenu(B_TRANSLATE("New"));
	menu->AddItem(new BMenuItem(fNewMenu, new BMessage(kMsgGenerateSudoku)));
	fNewMenu->Superitem()->SetShortcut('N', B_COMMAND_KEY);

	BMessage* message = new BMessage(kMsgGenerateSudoku);
	message->AddInt32("level", kEasyLevel);
	fNewMenu->AddItem(new BMenuItem(B_TRANSLATE("Easy"), message));
	message = new BMessage(kMsgGenerateSudoku);
	message->AddInt32("level", kAdvancedLevel);
	fNewMenu->AddItem(new BMenuItem(B_TRANSLATE("Advanced"), message));
	message = new BMessage(kMsgGenerateSudoku);
	message->AddInt32("level", kHardLevel);
	fNewMenu->AddItem(new BMenuItem(B_TRANSLATE("Hard"), message));

	fNewMenu->AddSeparatorItem();
	fNewMenu->AddItem(new BMenuItem(B_TRANSLATE("Blank"),
		new BMessage(kMsgNewBlank)));

	menu->AddItem(new BMenuItem(B_TRANSLATE("Start again"),
		new BMessage(kMsgStartAgain)));
	menu->AddSeparatorItem();
	BMenu* recentsMenu = BRecentFilesList::NewFileListMenu(
		B_TRANSLATE("Open file" B_UTF8_ELLIPSIS), NULL, NULL, this, 10, false,
		NULL, kSignature);
	BMenuItem *item;
	menu->AddItem(item = new BMenuItem(recentsMenu,
		new BMessage(kMsgOpenFilePanel)));
	item->SetShortcut('O', B_COMMAND_KEY);

	menu->AddSeparatorItem();

	BMenu* subMenu = new BMenu(B_TRANSLATE("Export as" B_UTF8_ELLIPSIS));
	message = new BMessage(kMsgExportAs);
	message->AddInt32("as", kExportAsText);
	subMenu->AddItem(new BMenuItem(B_TRANSLATE("Text"), message));
	message= new BMessage(kMsgExportAs);
	message->AddInt32("as", kExportAsHTML);
	subMenu->AddItem(new BMenuItem(B_TRANSLATE("HTML"), message));
	menu->AddItem(subMenu);

	menu->AddItem(item = new BMenuItem(B_TRANSLATE("Copy"),
		new BMessage(B_COPY), 'C'));

	menu->AddSeparatorItem();

	menu->AddItem(new BMenuItem(B_TRANSLATE("Quit"),
		new BMessage(B_QUIT_REQUESTED), 'Q'));
	menu->SetTargetForItems(this);
	item->SetTarget(be_app);
	menuBar->AddItem(menu);

	// "View" menu
	menu = new BMenu(B_TRANSLATE("View"));
	menu->AddItem(item = new BMenuItem(B_TRANSLATE("Mark invalid values"),
		new BMessage(kMsgMarkInvalid)));
	if ((fSudokuView->HintFlags() & kMarkInvalid) != 0)
		item->SetMarked(true);
	menu->AddItem(item = new BMenuItem(B_TRANSLATE("Mark valid hints"),
		new BMessage(kMsgMarkValidHints)));
	if ((fSudokuView->HintFlags() & kMarkValidHints) != 0)
		item->SetMarked(true);
	menu->SetTargetForItems(this);
	menuBar->AddItem(menu);

	// "Help" menu
	menu = new BMenu(B_TRANSLATE("Help"));
	menu->AddItem(fUndoItem = new BMenuItem(B_TRANSLATE("Undo"),
		new BMessage(B_UNDO), 'Z'));
	fUndoItem->SetEnabled(false);
	menu->AddItem(fRedoItem = new BMenuItem(B_TRANSLATE("Redo"),
		new BMessage(B_REDO), 'Z', B_SHIFT_KEY));
	fRedoItem->SetEnabled(false);
	menu->AddSeparatorItem();

	menu->AddItem(new BMenuItem(B_TRANSLATE("Snapshot current"),
		new BMessage(kMsgStoreState)));
	menu->AddItem(fRestoreStateItem = new BMenuItem(
		B_TRANSLATE("Restore snapshot"), new BMessage(kMsgRestoreState)));
	fRestoreStateItem->SetEnabled(fStoredState != NULL);
	menu->AddSeparatorItem();

	menu->AddItem(new BMenuItem(B_TRANSLATE("Set all hints"),
		new BMessage(kMsgSetAllHints)));
	menu->AddSeparatorItem();

	menu->AddItem(new BMenuItem(B_TRANSLATE("Solve"),
		new BMessage(kMsgSolveSudoku)));
	menu->AddItem(new BMenuItem(B_TRANSLATE("Solve single field"),
		new BMessage(kMsgSolveSingle)));
	menu->SetTargetForItems(fSudokuView);
	menuBar->AddItem(menu);

	fOpenPanel = new BFilePanel(B_OPEN_PANEL);
	fOpenPanel->SetTarget(this);
	fSavePanel = new BFilePanel(B_SAVE_PANEL);
	fSavePanel->SetTarget(this);

	_SetLevel(level);

	fSudokuView->StartWatching(this, kUndoRedoChanged);
		// we like to know whenever the undo/redo state changes

	fProgressWindow = new ProgressWindow(this,
		new BMessage(kMsgAbortSudokuGenerator));

	if (fSudokuView->Field()->IsEmpty())
		PostMessage(kMsgGenerateSudoku);
}


SudokuWindow::~SudokuWindow()
{
	delete fOpenPanel;
	delete fSavePanel;
	delete fGenerator;

	if (fProgressWindow->Lock())
		fProgressWindow->Quit();
}


status_t
SudokuWindow::_OpenSettings(BFile& file, uint32 mode)
{
	BPath path;
	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK)
		return B_ERROR;

	path.Append("Sudoku settings");

	return file.SetTo(path.Path(), mode);
}


status_t
SudokuWindow::_LoadSettings(BMessage& settings)
{
	BFile file;
	status_t status = _OpenSettings(file, B_READ_ONLY);
	if (status != B_OK)
		return status;

	return settings.Unflatten(&file);
}


status_t
SudokuWindow::_SaveSettings()
{
	BFile file;
	status_t status = _OpenSettings(file, B_WRITE_ONLY | B_CREATE_FILE
		| B_ERASE_FILE);
	if (status != B_OK)
		return status;

	BMessage settings('sudo');
	status = settings.AddRect("window frame", Frame());
	if (status == B_OK)
		status = fSudokuView->SaveState(settings);
	if (status == B_OK && fStoredState != NULL)
		status = settings.AddMessage("stored state", fStoredState);
	if (status == B_OK)
		status = settings.AddInt32("level", _Level());
	if (status == B_OK)
		status = settings.Flatten(&file);

	return status;
}


void
SudokuWindow::_ResetStoredState()
{
	delete fStoredState;
	fStoredState = NULL;
	fRestoreStateItem->SetEnabled(false);
}


void
SudokuWindow::_MessageDropped(BMessage* message)
{
	status_t status = B_MESSAGE_NOT_UNDERSTOOD;
	bool hasRef = false;

	entry_ref ref;
	if (message->FindRef("refs", &ref) != B_OK) {
		const void* data;
		ssize_t size;
		if (message->FindData("text/plain", B_MIME_TYPE, &data,
				&size) == B_OK) {
			status = fSudokuView->SetTo((const char*)data);
		} else
			return;
	} else {
		status = fSudokuView->SetTo(ref);
		if (status == B_OK)
			be_roster->AddToRecentDocuments(&ref, kSignature);

		BEntry entry(&ref);
		entry_ref parent;
		if (entry.GetParent(&entry) == B_OK
			&& entry.GetRef(&parent) == B_OK)
			fSavePanel->SetPanelDirectory(&parent);

		hasRef = true;
	}

	if (status < B_OK) {
		char buffer[1024];
		if (hasRef) {
			snprintf(buffer, sizeof(buffer),
				B_TRANSLATE("Could not open \"%s\":\n%s\n"), ref.name,
				strerror(status));
		} else {
			snprintf(buffer, sizeof(buffer),
				B_TRANSLATE("Could not set Sudoku:\n%s\n"),
				strerror(status));
		}

		BAlert* alert = new BAlert(B_TRANSLATE("Sudoku request"),
			buffer, B_TRANSLATE("OK"), NULL, NULL,
			B_WIDTH_AS_USUAL, B_STOP_ALERT);
		alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
		alert->Go();
	}
}


void
SudokuWindow::_Generate(int32 level)
{
	if (fGenerator != NULL)
		delete fGenerator;

	fSudokuView->SetEditable(false);
	fProgressWindow->Start(this);
	_ResetStoredState();

	fGenerator = new GenerateSudoku(*fSudokuView->Field(), level,
		fProgressWindow, this);
}


void
SudokuWindow::MessageReceived(BMessage* message)
{
	if (message->WasDropped()) {
		_MessageDropped(message);
		return;
	}

	switch (message->what) {
		case kMsgOpenFilePanel:
			fOpenPanel->Show();
			break;

		case B_REFS_RECEIVED:
		case B_SIMPLE_DATA:
			_MessageDropped(message);
			break;

		case kMsgGenerateSudoku:
		{
			int32 level;
			if (message->FindInt32("level", &level) != B_OK)
				level = _Level();

			_SetLevel(level);
			_Generate(level);
			break;
		}
		case kMsgAbortSudokuGenerator:
			if (fGenerator != NULL)
				fGenerator->Abort();
			break;
		case kMsgSudokuGenerated:
		{
			BMessage archive;
			if (message->FindMessage("field", &archive) == B_OK) {
				SudokuField* field = new SudokuField(&archive);
				fSudokuView->SetTo(field);
			}
			fSudokuView->SetEditable(true);
			fProgressWindow->Stop();

			delete fGenerator;
			fGenerator = NULL;
			break;
		}

		case kMsgExportAs:
		{
			if (message->FindInt32("as", (int32 *)&fExportFormat) < B_OK)
				fExportFormat = kExportAsText;
			fSavePanel->Show();
			break;
		}

		case B_COPY:
			fSudokuView->CopyToClipboard();
			break;

		case B_SAVE_REQUESTED:
		{
			entry_ref directoryRef;
			const char* name;
			if (message->FindRef("directory", &directoryRef) != B_OK
				|| message->FindString("name", &name) != B_OK)
				break;

			BDirectory directory(&directoryRef);
			BEntry entry(&directory, name);

			entry_ref ref;
			if (entry.GetRef(&ref) == B_OK)
				fSudokuView->SaveTo(ref, fExportFormat);
			break;
		}

		case kMsgNewBlank:
			_ResetStoredState();
			fSudokuView->ClearAll();
			break;

		case kMsgStartAgain:
			fSudokuView->ClearChanged();
			break;

		case kMsgMarkInvalid:
		case kMsgMarkValidHints:
		{
			BMenuItem* item;
			if (message->FindPointer("source", (void**)&item) != B_OK)
				return;

			uint32 flag = message->what == kMsgMarkInvalid
				? kMarkInvalid : kMarkValidHints;

			item->SetMarked(!item->IsMarked());
			if (item->IsMarked())
				fSudokuView->SetHintFlags(fSudokuView->HintFlags() | flag);
			else
				fSudokuView->SetHintFlags(fSudokuView->HintFlags() & ~flag);
			break;
		}

		case kMsgStoreState:
			delete fStoredState;
			fStoredState = new BMessage;
			fSudokuView->Field()->Archive(fStoredState, true);
			fRestoreStateItem->SetEnabled(true);
			break;

		case kMsgRestoreState:
		{
			if (fStoredState == NULL)
				break;

			SudokuField* field = new SudokuField(fStoredState);
			fSudokuView->SetTo(field);
			break;
		}

		case kMsgSudokuSolved:
		{
			BAlert* alert = new BAlert(B_TRANSLATE("Sudoku request"),
				B_TRANSLATE("Sudoku solved - congratulations!\n"),
				B_TRANSLATE("OK"), NULL, NULL,
				B_WIDTH_AS_USUAL, B_IDEA_ALERT);
			alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
			alert->Go();
			break;
		}

		case B_OBSERVER_NOTICE_CHANGE:
		{
			int32 what;
			if (message->FindInt32(B_OBSERVE_WHAT_CHANGE, &what) != B_OK)
				break;

			if (what == kUndoRedoChanged) {
				fUndoItem->SetEnabled(fSudokuView->CanUndo());
				fRedoItem->SetEnabled(fSudokuView->CanRedo());
			}
			break;
		}

		default:
			BWindow::MessageReceived(message);
			break;
	}
}


bool
SudokuWindow::QuitRequested()
{
	_SaveSettings();
	be_app->PostMessage(B_QUIT_REQUESTED);
	return true;
}


int32
SudokuWindow::_Level() const
{
	BMenuItem* item = fNewMenu->FindMarked();
	if (item == NULL)
		return 0;

	BMessage* message = item->Message();
	if (message == NULL)
		return 0;

	return message->FindInt32("level");
}


void
SudokuWindow::_SetLevel(int32 level)
{
	for (int32 i = 0; i < fNewMenu->CountItems(); i++) {
		BMenuItem* item = fNewMenu->ItemAt(i);

		BMessage* message = item->Message();
		if (message != NULL && message->HasInt32("level")
			&& message->FindInt32("level") == level)
			item->SetMarked(true);
		else
			item->SetMarked(false);
	}
}
