root/src/apps/sudoku/SudokuView.cpp
/*
 * Copyright 2007-2025, Axel Dörfler, axeld@pinc-software.de.
 * Distributed under the terms of the MIT License.
 */


#include "SudokuView.h"

#include "Sudoku.h"
#include "SudokuField.h"
#include "SudokuSolver.h"

#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

#include <Application.h>
#include <Beep.h>
#include <Bitmap.h>
#include <Clipboard.h>
#include <DataIO.h>
#include <Dragger.h>
#include <File.h>
#include <NodeInfo.h>
#include <Path.h>
#include <Picture.h>
#include <String.h>


static const uint32 kMsgCheckSolved = 'chks';

static const uint32 kStrongLineSize = 2;

static const rgb_color kHintColor = {255, 115, 0};
static const rgb_color kValueColor = {0, 91, 162};
static const rgb_color kValueCompletedColor = {55, 140, 35};
static const rgb_color kInvalidValueColor = {200, 0, 0};
static const rgb_color kIdealValueHintBackgroundColor = {255, 215, 127};
static const rgb_color kIdealHintValueHintBackgroundColor = {255, 235, 185};

extern const char* kSignature;


SudokuView::SudokuView(BRect frame, const char* name,
                const BMessage& settings, uint32 resizingMode)
        :
        BView(frame, name, resizingMode,
                B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE | B_FRAME_EVENTS)
{
        _InitObject(&settings);

#if 0
        BRect rect(Bounds());
        rect.top = rect.bottom - 7;
        rect.left = rect.right - 7;
        BDragger* dragger = new BDragger(rect, this);
        AddChild(dragger);
#endif
}


SudokuView::SudokuView(const char* name, const BMessage& settings)
        :
        BView(name,
                B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE | B_FRAME_EVENTS)
{
        _InitObject(&settings);
}


SudokuView::SudokuView(BMessage* archive)
        :
        BView(archive)
{
        _InitObject(archive);
}


SudokuView::~SudokuView()
{
        delete fField;
}


status_t
SudokuView::Archive(BMessage* into, bool deep) const
{
        status_t status;

        status = BView::Archive(into, deep);
        if (status < B_OK)
                return status;

        status = into->AddString("add_on", kSignature);
        if (status < B_OK)
                return status;

        status = into->AddRect("bounds", Bounds());
        if (status < B_OK)
                return status;

        status = SaveState(*into);
        if (status < B_OK)
                return status;
        return B_OK;
}


BArchivable*
SudokuView::Instantiate(BMessage* archive)
{
        if (!validate_instantiation(archive, "SudokuView"))
                return NULL;
        return new SudokuView(archive);
}


status_t
SudokuView::SaveState(BMessage& state) const
{
        BMessage field;
        status_t status = fField->Archive(&field, true);
        if (status == B_OK)
                status = state.AddMessage("field", &field);
        if (status == B_OK)
                status = state.AddInt32("hint flags", fHintFlags);
        if (status == B_OK)
                status = state.AddBool("show cursor", fShowCursor);

        return status;
}


status_t
SudokuView::SetTo(entry_ref& ref)
{
        BPath path;
        status_t status = path.SetTo(&ref);
        if (status < B_OK)
                return status;

        FILE* file = fopen(path.Path(), "r");
        if (file == NULL)
                return errno;

        uint32 maxOut = fField->Size() * fField->Size();
        char buffer[1024];
        char line[1024];
        bool ignore = false;
        uint32 out = 0;

        while (fgets(line, sizeof(line), file) != NULL
                && out < maxOut) {
                status = _FilterString(line, sizeof(line), buffer, out, ignore);
                if (status < B_OK) {
                        fclose(file);
                        return status;
                }
        }

        _PushUndo();
        status = fField->SetTo(_BaseCharacter(), buffer);
        fValueHintValue = UINT32_MAX;
        Invalidate();
        fclose(file);
        return status;
}


status_t
SudokuView::SetTo(const char* data)
{
        if (data == NULL)
                return B_BAD_VALUE;

        char buffer[1024];
        bool ignore = false;
        uint32 out = 0;

        status_t status = _FilterString(data, 65536, buffer, out, ignore);
        if (status < B_OK)
                return B_BAD_VALUE;

        _PushUndo();
        status = fField->SetTo(_BaseCharacter(), buffer);
        fValueHintValue = UINT32_MAX;
        Invalidate();
        return status;
}


status_t
SudokuView::SetTo(SudokuField* field)
{
        if (field == NULL || field == fField)
                return B_BAD_VALUE;

        _PushUndo();
        delete fField;
        fField = field;

        fBlockSize = fField->BlockSize();
        fValueHintValue = UINT32_MAX;
        FrameResized(0, 0);
        Invalidate();
        return B_OK;
}


status_t
SudokuView::SaveTo(entry_ref& ref, uint32 exportAs)
{
        BFile file;
        status_t status = file.SetTo(&ref, B_WRITE_ONLY | B_CREATE_FILE
                | B_ERASE_FILE);
        if (status < B_OK)
                return status;

        return SaveTo(file, exportAs);
}


status_t
SudokuView::SaveTo(BDataIO& stream, uint32 exportAs)
{
        BFile* file = dynamic_cast<BFile*>(&stream);
        uint32 i = 0;
        BNodeInfo nodeInfo;

        if (file)
                nodeInfo.SetTo(file);

        switch (exportAs) {
                case kExportAsText:
                {
                        BString text = "# Written by Sudoku\n\n";
                        stream.Write(text.String(), text.Length());

                        char* line = text.LockBuffer(1024);
                        memset(line, 0, 1024);
                        for (uint32 y = 0; y < fField->Size(); y++) {
                                for (uint32 x = 0; x < fField->Size(); x++) {
                                        if (x != 0 && x % fBlockSize == 0)
                                                line[i++] = ' ';
                                        _SetText(&line[i++], fField->ValueAt(x, y));
                                }
                                line[i++] = '\n';
                        }
                        text.UnlockBuffer();

                        stream.Write(text.String(), text.Length());
                        if (file)
                                nodeInfo.SetType("text/plain");
                        return B_OK;
                }

                case kExportAsHTML:
                {
                        bool netPositiveFriendly = false;
                        BString text = "<html>\n<head>\n<!-- Written by Sudoku -->\n"
                                "<style type=\"text/css\">\n"
                                "table.sudoku { background: #000000; border:0; border-collapse: "
                                        "collapse; cellpadding: 10px; cellspacing: 10px; width: "
                                        "300px; height: 300px; }\n"
                                "td.sudoku { background: #ffffff; border-color: black; "
                                        "border-left: none ; border-top: none ; /*border: none;*/ "
                                        "text-align: center; }\n"
                                "td.sudoku_initial {  }\n"
                                "td.sudoku_filled { color: blue; }\n"
                                "td.sudoku_empty {  }\n";

                        // border styles: right bottom (none, small or large)
                        const char* kStyles[] = {"none", "small", "large"};
                        const char* kCssStyles[] = {"none", "solid 1px black",
                                "solid 3px black"};
                        enum style_type { kNone = 0, kSmall, kLarge };

                        for (int32 right = 0; right < 3; right++) {
                                for (int32 bottom = 0; bottom < 3; bottom++) {
                                        text << "td.sudoku_";
                                        if (right != bottom)
                                                text << kStyles[right] << "_" << kStyles[bottom];
                                        else
                                                text << kStyles[right];

                                        text << " { border-right: " << kCssStyles[right]
                                                << "; border-bottom: " << kCssStyles[bottom] << "; }\n";
                                }
                        }

                        text << "</style>\n"
                                "</head>\n<body>\n\n";
                        stream.Write(text.String(), text.Length());

                        text = "<table";
                        if (netPositiveFriendly)
                                text << " border=\"1\"";
                        text << " class=\"sudoku\">";
                        stream.Write(text.String(), text.Length());

                        text = "";
                        BString divider;
                        divider << (int)(100.0 / fField->Size()) << "%";
                        for (uint32 y = 0; y < fField->Size(); y++) {
                                text << "<tr height=\"" << divider << "\">\n";
                                for (uint32 x = 0; x < fField->Size(); x++) {
                                        char buff[2];
                                        _SetText(buff, fField->ValueAt(x, y));

                                        BString style = "sudoku_";
                                        style_type right = kSmall;
                                        style_type bottom = kSmall;
                                        if ((x + 1) % fField->BlockSize() == 0)
                                                right = kLarge;
                                        if ((y + 1) % fField->BlockSize() == 0)
                                                bottom = kLarge;
                                        if (x == fField->Size() - 1)
                                                right = kNone;
                                        if (y == fField->Size() - 1)
                                                bottom = kNone;

                                        if (right != bottom)
                                                style << kStyles[right] << "_" << kStyles[bottom];
                                        else
                                                style << kStyles[right];

                                        if (fField->ValueAt(x, y) == 0) {
                                                text << "<td width=\"" << divider << "\" ";
                                                text << "class=\"sudoku sudoku_empty " << style;
                                                text << "\">\n&nbsp;";
                                        } else if (fField->IsInitialValue(x, y)) {
                                                text << "<td width=\"" << divider << "\" ";
                                                text << "class=\"sudoku sudoku_initial " << style
                                                        << "\">\n";
                                                if (netPositiveFriendly)
                                                        text << "<font color=\"#000000\">";
                                                text << buff;
                                                if (netPositiveFriendly)
                                                        text << "</font>";
                                        } else {
                                                text << "<td width=\"" << divider << "\" ";
                                                text << "class=\"sudoku sudoku_filled sudoku_" << style
                                                        << "\">\n";
                                                if (netPositiveFriendly)
                                                        text << "<font color=\"#0000ff\">";
                                                text << buff;
                                                if (netPositiveFriendly)
                                                        text << "</font>";
                                        }
                                        text << "</td>\n";
                                }
                                text << "</tr>\n";
                        }
                        text << "</table>\n\n";

                        stream.Write(text.String(), text.Length());
                        text = "</body></html>\n";
                        stream.Write(text.String(), text.Length());
                        if (file)
                                nodeInfo.SetType("text/html");
                        return B_OK;
                }

                case kExportAsBitmap:
                {
                        BMallocIO mallocIO;
                        status_t status = SaveTo(mallocIO, kExportAsPicture);
                        if (status < B_OK)
                                return status;

                        mallocIO.Seek(0LL, SEEK_SET);
                        BPicture picture;
                        status = picture.Unflatten(&mallocIO);
                        if (status < B_OK)
                                return status;

                        BBitmap* bitmap = new BBitmap(Bounds(), B_BITMAP_ACCEPTS_VIEWS,
                                B_RGB32);
                        BView* view = new BView(Bounds(), "bitmap", B_FOLLOW_NONE,
                                B_WILL_DRAW);
                        bitmap->AddChild(view);

                        if (bitmap->Lock()) {
                                view->DrawPicture(&picture);
                                view->Sync();

                                view->RemoveSelf();
                                delete view;
                                        // it should not become part of the archive
                                bitmap->Unlock();
                        }

                        BMessage archive;
                        status = bitmap->Archive(&archive);
                        if (status >= B_OK)
                                status = archive.Flatten(&stream);

                        delete bitmap;
                        return status;
                }

                case kExportAsPicture:
                {
                        BPicture picture;
                        BeginPicture(&picture);
                        Draw(Bounds());

                        status_t status = B_ERROR;
                        if (EndPicture())
                                status = picture.Flatten(&stream);

                        return status;
                }

                default:
                        return B_BAD_VALUE;
        }
}


status_t
SudokuView::CopyToClipboard()
{
        if (!be_clipboard->Lock())
                return B_ERROR;

        be_clipboard->Clear();

        BMessage* clip = be_clipboard->Data();
        if (clip == NULL) {
                be_clipboard->Unlock();
                return B_ERROR;
        }

        // As BBitmap
        BMallocIO mallocIO;
        status_t status = SaveTo(mallocIO, kExportAsBitmap);
        if (status >= B_OK) {
                mallocIO.Seek(0LL, SEEK_SET);
                // ShowImage, ArtPaint & WonderBrush use that
                status = clip->AddData("image/bitmap", B_MESSAGE_TYPE,
                        mallocIO.Buffer(), mallocIO.BufferLength());
                // Becasso uses that ?
                clip->AddData("image/x-be-bitmap", B_MESSAGE_TYPE, mallocIO.Buffer(),
                        mallocIO.BufferLength());
                // Gobe Productive uses that...
                // QuickRes as well, with a rect field.
                clip->AddData("image/x-vnd.Be-bitmap", B_MESSAGE_TYPE,
                        mallocIO.Buffer(), mallocIO.BufferLength());
        }
        mallocIO.Seek(0LL, SEEK_SET);
        mallocIO.SetSize(0LL);

        // As HTML
        if (status >= B_OK)
                status = SaveTo(mallocIO, kExportAsHTML);
        if (status >= B_OK) {
                status = clip->AddData("text/html", B_MIME_TYPE, mallocIO.Buffer(),
                        mallocIO.BufferLength());
        }
        mallocIO.Seek(0LL, SEEK_SET);
        mallocIO.SetSize(0LL);

        // As plain text
        if (status >= B_OK)
                SaveTo(mallocIO, kExportAsText);
        if (status >= B_OK) {
                status = clip->AddData("text/plain", B_MIME_TYPE, mallocIO.Buffer(),
                        mallocIO.BufferLength());
        }
        mallocIO.Seek(0LL, SEEK_SET);
        mallocIO.SetSize(0LL);

        // As flattened BPicture, anyone handles that?
        if (status >= B_OK)
                status = SaveTo(mallocIO, kExportAsPicture);
        if (status >= B_OK) {
                status = clip->AddData("image/x-vnd.Be-picture", B_MIME_TYPE,
                        mallocIO.Buffer(), mallocIO.BufferLength());
        }
        mallocIO.SetSize(0LL);

        be_clipboard->Commit();
        be_clipboard->Unlock();

        return status;
}


void
SudokuView::ClearChanged()
{
        _PushUndo();

        for (uint32 y = 0; y < fField->Size(); y++) {
                for (uint32 x = 0; x < fField->Size(); x++) {
                        if (!fField->IsInitialValue(x, y))
                                fField->SetValueAt(x, y, 0);
                }
        }

        Invalidate();
}


void
SudokuView::ClearAll()
{
        _PushUndo();
        fField->Reset();
        Invalidate();
}


void
SudokuView::SetHintFlags(uint32 flags)
{
        if (flags == fHintFlags)
                return;

        if ((flags & kMarkInvalid) ^ (fHintFlags & kMarkInvalid))
                Invalidate();

        fHintFlags = flags;
}


void
SudokuView::SetEditable(bool editable)
{
        fEditable = editable;
}


void
SudokuView::Undo()
{
        _UndoRedo(fUndos, fRedos);
}


void
SudokuView::Redo()
{
        _UndoRedo(fRedos, fUndos);
}


// #pragma mark - BWindow methods


void
SudokuView::AttachedToWindow()
{
        MakeFocus(true);
}


void
SudokuView::FrameResized(float /*width*/, float /*height*/)
{
        // font for numbers

        uint32 size = fField->Size();
        fWidth = (Bounds().Width() + 2 - kStrongLineSize * (fBlockSize - 1)) / size;
        fHeight = (Bounds().Height() + 2 - kStrongLineSize * (fBlockSize - 1))
                / size;
        _FitFont(fFieldFont, fWidth - 2, fHeight - 2);

        font_height fontHeight;
        fFieldFont.GetHeight(&fontHeight);
        fBaseline = ceilf(fontHeight.ascent) / 2
                + (fHeight - ceilf(fontHeight.descent)) / 2;

        // font for hint

        fHintWidth = (fWidth - 2) / fBlockSize;
        fHintHeight = (fHeight - 2) / fBlockSize;
        _FitFont(fHintFont, fHintWidth, fHintHeight);

        fHintFont.GetHeight(&fontHeight);
        fHintBaseline = ceilf(fontHeight.ascent) / 2
                + (fHintHeight - ceilf(fontHeight.descent)) / 2;

        // fix the dragger's position
        BView *dragger = FindView("_dragger_");
        if (dragger)
                dragger->MoveTo(Bounds().right - 7, Bounds().bottom - 7);
}


void
SudokuView::MouseDown(BPoint where)
{
        uint32 x, y;
        if (!fEditable || !_GetFieldFor(where, x, y))
                return;

        int32 buttons = B_PRIMARY_MOUSE_BUTTON;
        int32 clicks = 1;
        if (Looper() != NULL && Looper()->CurrentMessage() != NULL) {
                Looper()->CurrentMessage()->FindInt32("buttons", &buttons);
                Looper()->CurrentMessage()->FindInt32("clicks", &clicks);
        }

        if (buttons == B_PRIMARY_MOUSE_BUTTON && clicks == 1) {
                uint32 value = fField->ValueAt(x, y);
                if (value != 0) {
                        _SetValueHintValue(value);
                        return;
                }
        }

        uint32 hintX, hintY;
        if (!_GetHintFieldFor(where, x, y, hintX, hintY))
                return;

        uint32 value = hintX + hintY * fBlockSize;
        uint32 field = x + y * fField->Size();
        _PushUndo();
        _SetValueHintValue(value + 1);

        if ((clicks == 2 && fLastHintValue == value && fLastField == field)
                || (buttons & (B_SECONDARY_MOUSE_BUTTON
                                | B_TERTIARY_MOUSE_BUTTON)) != 0) {
                // Double click or other buttons set or remove a value
                if (!fField->IsInitialValue(x, y))
                        _SetValue(x, y, fField->ValueAt(x, y) == 0 ? value + 1 : 0);

                return;
        }

        _ToggleHintValue(x, y, hintX, hintY, value, field);
}


void
SudokuView::MouseMoved(BPoint where, uint32 transit,
        const BMessage* dragMessage)
{
        if (transit == B_EXITED_VIEW || dragMessage != NULL) {
                _RemoveHint();
                return;
        }

        if (fShowKeyboardFocus) {
                fShowKeyboardFocus = false;
                _InvalidateKeyboardFocus(fKeyboardX, fKeyboardY);
        }

        uint32 x, y;
        bool isField = _GetFieldFor(where, x, y);
        if (isField) {
                fKeyboardX = x;
                fKeyboardY = y;
        }

        if (!isField
                || fField->IsInitialValue(x, y)
                || (!fShowCursor && fField->ValueAt(x, y) != 0)) {
                _RemoveHint();
                return;
        }

        if (fShowHintX == x && fShowHintY == y)
                return;

        int32 buttons = 0;
        if (Looper() != NULL && Looper()->CurrentMessage() != NULL)
                Looper()->CurrentMessage()->FindInt32("buttons", &buttons);

        uint32 field = x + y * fField->Size();

        if (buttons != 0 && field != fLastField) {
                // if a button is pressed, we drag the last hint selection
                // (either set or removal) to the field under the mouse
                uint32 hintMask = fField->HintMaskAt(x, y);
                uint32 valueMask = 1UL << fLastHintValue;
                if (fLastHintValueSet)
                        hintMask |= valueMask;
                else
                        hintMask &= ~valueMask;

                fField->SetHintMaskAt(x, y, hintMask);
        }

        _RemoveHint();
        fShowHintX = x;
        fShowHintY = y;
        _InvalidateField(x, y);
}


void
SudokuView::KeyDown(const char *bytes, int32 /*numBytes*/)
{
        be_app->ObscureCursor();

        uint32 x = fKeyboardX, y = fKeyboardY;

        switch (bytes[0]) {
                case B_UP_ARROW:
                        if (fKeyboardY == 0)
                                fKeyboardY = fField->Size() - 1;
                        else
                                fKeyboardY--;
                        break;
                case B_DOWN_ARROW:
                        if (fKeyboardY == fField->Size() - 1)
                                fKeyboardY = 0;
                        else
                                fKeyboardY++;
                        break;

                case B_LEFT_ARROW:
                        if (fKeyboardX == 0)
                                fKeyboardX = fField->Size() - 1;
                        else
                                fKeyboardX--;
                        break;
                case B_RIGHT_ARROW:
                        if (fKeyboardX == fField->Size() - 1)
                                fKeyboardX = 0;
                        else
                                fKeyboardX++;
                        break;

                case B_BACKSPACE:
                case B_DELETE:
                case B_SPACE:
                        // clear value
                        _InsertKey(_BaseCharacter(), 0);
                        break;

                default:
                        int32 rawKey = bytes[0];
                        int32 modifiers = 0;
                        if (Looper() != NULL && Looper()->CurrentMessage() != NULL) {
                                Looper()->CurrentMessage()->FindInt32("raw_char", &rawKey);
                                Looper()->CurrentMessage()->FindInt32("modifiers", &modifiers);
                        }

                        _InsertKey(rawKey, modifiers);
                        break;
        }

        if (!fShowKeyboardFocus && fShowHintX != UINT32_MAX) {
                // always start at last mouse position, if any
                fKeyboardX = fShowHintX;
                fKeyboardY = fShowHintY;
        }

        _RemoveHint();

        // remove old focus, if any
        if (fShowKeyboardFocus && (x != fKeyboardX || y != fKeyboardY))
                _InvalidateKeyboardFocus(x, y);

        fShowKeyboardFocus = true;
        _InvalidateKeyboardFocus(fKeyboardX, fKeyboardY);
}


void
SudokuView::MessageReceived(BMessage* message)
{
        switch (message->what) {
                case kMsgCheckSolved:
                        if (fField->IsSolved()) {
                                // notify window
                                Looper()->PostMessage(kMsgSudokuSolved);
                        }
                        break;

                case B_UNDO:
                        Undo();
                        break;

                case B_REDO:
                        Redo();
                        break;

                case kMsgSetAllHints:
                        _SetAllHints();
                        break;

                case kMsgSolveSudoku:
                        _Solve();
                        break;

                case kMsgSolveSingle:
                        _SolveSingle();
                        break;

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


void
SudokuView::Draw(BRect /*updateRect*/)
{
        // draw lines

        const color_which kBackgroundColorWhich = B_DOCUMENT_BACKGROUND_COLOR;
        const rgb_color kBackgroundColor = ui_color(kBackgroundColorWhich);
        const color_which kTextColorWhich = B_DOCUMENT_TEXT_COLOR;
        const rgb_color kTextColor = ui_color(kTextColorWhich);

        SetLowUIColor(kBackgroundColorWhich);
        SetHighUIColor(B_CONTROL_BORDER_COLOR);

        uint32 size = fField->Size();
        float width = fWidth - 1;
        for (uint32 x = 1; x < size; x++) {
                if (x % fBlockSize == 0) {
                        FillRect(BRect(width, 0, width + kStrongLineSize,
                                Bounds().Height()));
                        width += kStrongLineSize;
                } else {
                        StrokeLine(BPoint(width, 0), BPoint(width, Bounds().Height()));
                }
                width += fWidth;
        }

        float height = fHeight - 1;
        for (uint32 y = 1; y < size; y++) {
                if (y % fBlockSize == 0) {
                        FillRect(BRect(0, height, Bounds().Width(),
                                height + kStrongLineSize));
                        height += kStrongLineSize;
                } else {
                        StrokeLine(BPoint(0, height), BPoint(Bounds().Width(), height));
                }
                height += fHeight;
        }

        // draw text

        for (uint32 y = 0; y < size; y++) {
                for (uint32 x = 0; x < size; x++) {
                        uint32 value = fField->ValueAt(x, y);
                        rgb_color fieldBackgroundColor = kBackgroundColor;

                        if (value == fValueHintValue) {
                                fieldBackgroundColor = mix_color(kBackgroundColor,
                                        kIdealValueHintBackgroundColor, 100);
                        } else if (value == 0 && fField->HasHint(x, y, fValueHintValue)) {
                                fieldBackgroundColor = mix_color(kBackgroundColor,
                                        kIdealHintValueHintBackgroundColor, 100);
                        }

                        if (((fShowCursor && x == fShowHintX && y == fShowHintY)
                                        || (fShowKeyboardFocus && x == fKeyboardX
                                                && y == fKeyboardY))
                                && !fField->IsInitialValue(x, y)) {
                                // TODO: make color more intense
                                SetLowColor(tint_color(fieldBackgroundColor, B_DARKEN_2_TINT));
                                FillRect(_Frame(x, y), B_SOLID_LOW);
                        } else {
                                SetLowColor(fieldBackgroundColor);
                                FillRect(_Frame(x, y), B_SOLID_LOW);
                        }

                        if (fShowKeyboardFocus && x == fKeyboardX && y == fKeyboardY)
                                _DrawKeyboardFocus();

                        if (value == 0) {
                                _DrawHints(x, y);
                                continue;
                        }

                        SetFont(&fFieldFont);
                        if (fField->IsInitialValue(x, y))
                                SetHighUIColor(kTextColorWhich);
                        else {
                                if ((fHintFlags & kMarkInvalid) == 0
                                        || fField->IsValid(x, y, value)) {
                                        if (fField->IsValueCompleted(value)) {
                                                if (kTextColor.IsDark())
                                                        SetHighColor(kValueCompletedColor);
                                                else
                                                        SetHighColor(tint_color(kValueCompletedColor, B_LIGHTEN_2_TINT));
                                        } else {
                                                if (kTextColor.IsDark())
                                                        SetHighColor(kValueColor);
                                                else
                                                        SetHighColor(tint_color(kValueColor, B_LIGHTEN_2_TINT));
                                        }
                                } else {
                                        if (kTextColor.IsDark())
                                                SetHighColor(kInvalidValueColor);
                                        else
                                                SetHighColor(tint_color(kInvalidValueColor, B_LIGHTEN_2_TINT));
                                }
                        }

                        char text[2];
                        _SetText(text, value);
                        DrawString(text, _LeftTop(x, y)
                                + BPoint((fWidth - StringWidth(text)) / 2, fBaseline));
                }
        }
}


// #pragma mark - Private methods


void
SudokuView::_InitObject(const BMessage* archive)
{
        fField = NULL;
        fShowHintX = UINT32_MAX;
        fValueHintValue = UINT32_MAX;
        fLastHintValue = UINT32_MAX;
        fLastField = UINT32_MAX;
        fKeyboardX = 0;
        fKeyboardY = 0;
        fShowKeyboardFocus = false;
        fEditable = true;

        BMessage field;
        if (archive->FindMessage("field", &field) == B_OK) {
                fField = new SudokuField(&field);
                if (fField->InitCheck() != B_OK) {
                        delete fField;
                        fField = NULL;
                } else if (fField->IsSolved())
                        ClearAll();
        }
        if (fField == NULL)
                fField = new SudokuField(3);

        fBlockSize = fField->BlockSize();

        if (archive->FindInt32("hint flags", (int32*)&fHintFlags) != B_OK)
                fHintFlags = kMarkInvalid;
        if (archive->FindBool("show cursor", &fShowCursor) != B_OK)
                fShowCursor = false;

        SetViewColor(B_TRANSPARENT_COLOR);
                // to avoid flickering
        SetLowUIColor(B_CONTROL_BACKGROUND_COLOR);
        FrameResized(0, 0);
}


status_t
SudokuView::_FilterString(const char* data, size_t dataLength, char* buffer,
        uint32& out, bool& ignore)
{
        uint32 maxOut = fField->Size() * fField->Size();

        for (uint32 i = 0; i < dataLength && data[i]; i++) {
                if (data[i] == '#')
                        ignore = true;
                else if (data[i] == '\n')
                        ignore = false;

                if (ignore || isspace(data[i]))
                        continue;

                if (!_ValidCharacter(data[i])) {
                        return B_BAD_VALUE;
                }

                buffer[out++] = data[i];
                if (out == maxOut)
                        break;
        }

        buffer[out] = '\0';
        return B_OK;
}


void
SudokuView::_SetText(char* text, uint32 value)
{
        text[0] = value + _BaseCharacter();
        text[1] = '\0';
}


char
SudokuView::_BaseCharacter()
{
        return fField->Size() > 9 ? '@' : '0';
}


bool
SudokuView::_ValidCharacter(char c)
{
        char min = _BaseCharacter();
        char max = min + fField->Size();
        return c >= min && c <= max;
}


BPoint
SudokuView::_LeftTop(uint32 x, uint32 y)
{
        return BPoint(x * fWidth - 1 + x / fBlockSize * kStrongLineSize + 1,
                y * fHeight - 1 + y / fBlockSize * kStrongLineSize + 1);
}


BRect
SudokuView::_Frame(uint32 x, uint32 y)
{
        BPoint leftTop = _LeftTop(x, y);
        BPoint rightBottom = leftTop + BPoint(fWidth - 2, fHeight - 2);

        return BRect(leftTop, rightBottom);
}


void
SudokuView::_InvalidateHintField(uint32 x, uint32 y, uint32 hintX,
        uint32 hintY)
{
        BPoint leftTop = _LeftTop(x, y);
        leftTop.x += hintX * fHintWidth;
        leftTop.y += hintY * fHintHeight;
        BPoint rightBottom = leftTop;
        rightBottom.x += fHintWidth;
        rightBottom.y += fHintHeight;

        Invalidate(BRect(leftTop, rightBottom));
}


void
SudokuView::_InvalidateField(uint32 x, uint32 y)
{
        Invalidate(_Frame(x, y));
}


void
SudokuView::_InvalidateValue(uint32 value, bool invalidateHint,
        uint32 fieldX, uint32 fieldY)
{
        for (uint32 y = 0; y < fField->Size(); y++) {
                for (uint32 x = 0; x < fField->Size(); x++) {
                        if (fField->ValueAt(x, y) == value || (x == fieldX && y == fieldY))
                                Invalidate(_Frame(x, y));
                        else if (invalidateHint && fField->ValueAt(x, y) == 0
                                && fField->HasHint(x, y, value))
                                Invalidate(_Frame(x, y));
                }
        }
}


void
SudokuView::_InvalidateKeyboardFocus(uint32 x, uint32 y)
{
        BRect frame = _Frame(x, y);
        frame.InsetBy(-1, -1);
        Invalidate(frame);
}


void
SudokuView::_InsertKey(char rawKey, int32 modifiers)
{
        if (!fEditable || !_ValidCharacter(rawKey)
                || fField->IsInitialValue(fKeyboardX, fKeyboardY))
                return;

        uint32 value = rawKey - _BaseCharacter();

        if (modifiers & (B_SHIFT_KEY | B_OPTION_KEY)) {
                // set or remove hint
                if (value == 0)
                        return;

                _PushUndo();
                uint32 hintMask = fField->HintMaskAt(fKeyboardX, fKeyboardY);
                uint32 valueMask = 1UL << (value - 1);
                if (modifiers & B_OPTION_KEY)
                        hintMask &= ~valueMask;
                else
                        hintMask |= valueMask;

                fField->SetValueAt(fKeyboardX, fKeyboardY, 0);
                fField->SetHintMaskAt(fKeyboardX, fKeyboardY, hintMask);
        } else {
                _PushUndo();
                _SetValue(fKeyboardX, fKeyboardY, value);
        }
}


bool
SudokuView::_GetHintFieldFor(BPoint where, uint32 x, uint32 y,
        uint32& hintX, uint32& hintY)
{
        BPoint leftTop = _LeftTop(x, y);
        hintX = (uint32)floor((where.x - leftTop.x) / fHintWidth);
        hintY = (uint32)floor((where.y - leftTop.y) / fHintHeight);

        if (hintX >= fBlockSize || hintY >= fBlockSize)
                return false;

        return true;
}


bool
SudokuView::_GetFieldFor(BPoint where, uint32& x, uint32& y)
{
        float block = fWidth * fBlockSize + kStrongLineSize;
        x = (uint32)floor(where.x / block);
        uint32 offsetX = (uint32)floor((where.x - x * block) / fWidth);
        x = x * fBlockSize + offsetX;

        block = fHeight * fBlockSize + kStrongLineSize;
        y = (uint32)floor(where.y / block);
        uint32 offsetY = (uint32)floor((where.y - y * block) / fHeight);
        y = y * fBlockSize + offsetY;

        if (offsetX >= fBlockSize || offsetY >= fBlockSize
                || x >= fField->Size() || y >= fField->Size())
                return false;

        return true;
}


void
SudokuView::_SetValue(uint32 x, uint32 y, uint32 value)
{
        bool wasCompleted;
        if (value == 0) {
                // Remove value
                value = fField->ValueAt(x, y);
                wasCompleted = fField->IsValueCompleted(value);

                fField->SetValueAt(x, y, 0);
                fShowHintX = x;
                fShowHintY = y;
        } else {
                // Set value
                wasCompleted = fField->IsValueCompleted(value);

                fField->SetValueAt(x, y, value);
                BMessenger(this).SendMessage(kMsgCheckSolved);

                _RemoveHintValues(x, y, value);

                // allow dragging to remove the hint from other fields
                fLastHintValueSet = false;
                fLastHintValue = value - 1;
                fLastField = x + y * fField->Size();
        }

        if (value != fValueHintValue && fValueHintValue != UINT32_MAX)
                _SetValueHintValue(value);

        if (wasCompleted != fField->IsValueCompleted(value))
                _InvalidateValue(value, false, x, y);
        else
                _InvalidateField(x, y);
}


void
SudokuView::_ToggleHintValue(uint32 x, uint32 y, uint32 hintX, uint32 hintY,
        uint32 value, uint32 field)
{
        uint32 hintMask = fField->HintMaskAt(x, y);
        uint32 valueMask = 1UL << value;
        fLastHintValueSet = (hintMask & valueMask) == 0;

        if (fLastHintValueSet)
                hintMask |= valueMask;
        else
                hintMask &= ~valueMask;

        fField->SetHintMaskAt(x, y, hintMask);

        if (value + 1 != fValueHintValue) {
                _SetValueHintValue(UINT32_MAX);
                _InvalidateHintField(x, y, hintX, hintY);
        } else
                _InvalidateField(x, y);

        fLastHintValue = value;
        fLastField = field;
}


void
SudokuView::_RemoveHintValues(uint32 atX, uint32 atY, uint32 value)
{
        // Remove all hints in the same block
        uint32 blockSize = fField->BlockSize();
        uint32 blockX = (atX / blockSize) * blockSize;
        uint32 blockY = (atY / blockSize) * blockSize;
        uint32 valueMask = 1UL << (value - 1);

        for (uint32 y = blockY; y < blockY + blockSize; y++) {
                for (uint32 x = blockX; x < blockX + blockSize; x++) {
                        if (x != atX && y != atY)
                                _RemoveHintValue(x, y, valueMask);
                }
        }

        // Remove all hints from the vertical and horizontal lines

        for (uint32 i = 0; i < fField->Size(); i++) {
                if (i != atX)
                        _RemoveHintValue(i, atY, valueMask);
                if (i != atY)
                        _RemoveHintValue(atX, i, valueMask);
        }
}


void
SudokuView::_RemoveHintValue(uint32 x, uint32 y, uint32 valueMask)
{
        uint32 hintMask = fField->HintMaskAt(x, y);
        if ((hintMask & valueMask) != 0) {
                fField->SetHintMaskAt(x, y, hintMask & ~valueMask);
                _InvalidateField(x, y);
        }
}


void
SudokuView::_SetAllHints()
{
        uint32 size = fField->Size();

        for (uint32 y = 0; y < size; y++) {
                for (uint32 x = 0; x < size; x++) {
                        uint32 validMask = fField->ValidMaskAt(x, y);
                        fField->SetHintMaskAt(x, y, validMask);
                }
        }
        Invalidate();
}


void
SudokuView::_Solve()
{
        SudokuSolver solver;
        if (_GetSolutions(solver)) {
                _PushUndo();
                fField->SetTo(solver.SolutionAt(0));
                Invalidate();
        } else
                beep();
}


void
SudokuView::_SolveSingle()
{
        if (fField->IsSolved()) {
                beep();
                return;
        }

        SudokuSolver solver;
        if (_GetSolutions(solver)) {
                _PushUndo();

                // find free spot
                uint32 x, y;
                do {
                        x = rand() % fField->Size();
                        y = rand() % fField->Size();
                } while (fField->ValueAt(x, y));

                uint32 value = solver.SolutionAt(0)->ValueAt(x, y);
                _SetValue(x, y, value);
        } else
                beep();
}


bool
SudokuView::_GetSolutions(SudokuSolver& solver)
{
        solver.SetTo(fField);
        bigtime_t start = system_time();
        solver.ComputeSolutions();

        printf("found %" B_PRIu32 " solutions in %g msecs\n",
                solver.CountSolutions(), (system_time() - start) / 1000.0);

        return solver.CountSolutions() > 0;
}


void
SudokuView::_UndoRedo(BObjectList<BMessage>& undos,
        BObjectList<BMessage>& redos)
{
        if (undos.IsEmpty())
                return;

        BMessage* undo = undos.RemoveItemAt(undos.CountItems() - 1);

        BMessage* redo = new BMessage;
        if (fField->Archive(redo, true) == B_OK)
                redos.AddItem(redo);

        SudokuField field(undo);
        delete undo;

        fField->SetTo(&field);

        SendNotices(kUndoRedoChanged);
        Invalidate();
}


void
SudokuView::_PushUndo()
{
        fRedos.MakeEmpty();

        BMessage* undo = new BMessage;
        if (fField->Archive(undo, true) == B_OK
                && fUndos.AddItem(undo))
                SendNotices(kUndoRedoChanged);
}


void
SudokuView::_SetValueHintValue(uint32 value)
{
        if (value == fValueHintValue)
                return;

        if (fValueHintValue != UINT32_MAX)
                _InvalidateValue(fValueHintValue, true);

        fValueHintValue = value;

        if (fValueHintValue != UINT32_MAX)
                _InvalidateValue(fValueHintValue, true);
}


void
SudokuView::_RemoveHint()
{
        if (fShowHintX == UINT32_MAX)
                return;

        uint32 x = fShowHintX;
        uint32 y = fShowHintY;
        fShowHintX = UINT32_MAX;
        fShowHintY = UINT32_MAX;

        _InvalidateField(x, y);
}


void
SudokuView::_FitFont(BFont& font, float fieldWidth, float fieldHeight)
{
        font.SetSize(100);

        font_height fontHeight;
        font.GetHeight(&fontHeight);

        float width = font.StringWidth("W");
        float height = ceilf(fontHeight.ascent) + ceilf(fontHeight.descent);

        float factor = fieldWidth != fHintWidth ? 4.f / 5.f : 1.f;
        float widthFactor = fieldWidth / (width / factor);
        float heightFactor = fieldHeight / (height / factor);
        font.SetSize(100 * min_c(widthFactor, heightFactor));
}


void
SudokuView::_DrawKeyboardFocus()
{
        BRect frame = _Frame(fKeyboardX, fKeyboardY);
        SetHighColor(ui_color(B_KEYBOARD_NAVIGATION_COLOR));
        StrokeRect(frame);
        frame.InsetBy(-1, -1);
        StrokeRect(frame);
        frame.InsetBy(2, 2);
        StrokeRect(frame);
}


void
SudokuView::_DrawHints(uint32 x, uint32 y)
{
        bool showAll = fShowHintX == x && fShowHintY == y;
        uint32 hintMask = fField->HintMaskAt(x, y);
        if (hintMask == 0 && !showAll)
                return;

        uint32 validMask = fField->ValidMaskAt(x, y);
        BPoint leftTop = _LeftTop(x, y);
        SetFont(&fHintFont);

        for (uint32 j = 0; j < fBlockSize; j++) {
                for (uint32 i = 0; i < fBlockSize; i++) {
                        uint32 value = j * fBlockSize + i;
                        if ((hintMask & (1UL << value)) != 0) {
                                SetHighColor(kHintColor);
                        } else {
                                if (!showAll)
                                        continue;

                                if ((fHintFlags & kMarkValidHints) == 0
                                        || (validMask & (1UL << value)) != 0)
                                        SetHighColor(110, 110, 80);
                                else
                                        SetHighColor(180, 180, 120);
                        }

                        char text[2];
                        _SetText(text, value + 1);
                        DrawString(text, leftTop + BPoint((i + 0.5f) * fHintWidth
                                - StringWidth(text) / 2, floorf(j * fHintHeight)
                                        + fHintBaseline));
                }
        }
}