#include "wxvbam.h"
#include "viewsupt.h"

namespace Viewers
{
    void Viewer::CloseDlg(wxCloseEvent &ev)
    {
	// stop tracking dialog
	MainFrame *f = wxGetApp().frame;
	for(dialog_list_t::iterator i = f->popups.begin();
	    i != f->popups.end(); i++)
	    if(*i == this) {
		f->popups.erase(i);
		break;
	    }
	// just forwarding this event does not cause system to close window
	// ev.Skip();
	// so do it manually
	Destroy();
    }

    Viewer::Viewer(const wxChar *name) : wxDialog(), auto_update(false)
    {
	dname = name;
	MainFrame *f = wxGetApp().frame;
	// using LoadDialog precludes non-wxDialog nodes, which is good
	// since Viewer is strictly wxDialog-derived.
	if(!wxXmlResource::Get()->LoadDialog(this, f, name))
	    baddialog();
	f->popups.push_back(this);
	SetEscapeId(wxID_CLOSE);
	Fit();
    }

    void Viewer::ActiveCtrl(wxCommandEvent &ev)
    {
	wxWindow *ctrl = wxStaticCast(ev.GetEventObject(), wxWindow);
	// TransferDataFromWindow() only operates on children, not self
	// so just simulate
	wxValidator *v = ctrl->GetValidator();
	if(v) {
	    v->TransferFromWindow();
	    Update();
	}
    }

    BEGIN_EVENT_TABLE(Viewer, wxDialog)
	EVT_CLOSE(Viewer::CloseDlg)
        EVT_CHECKBOX(XRCID("AutoUpdate"), Viewer::SetAutoUpdate)
	EVT_RADIOBUTTON(wxID_ANY, Viewer::ActiveCtrl)
        EVT_SPINCTRL(wxID_ANY, Viewer::ActiveCtrlSpin)
	// this does not interfere with/intercept disassemble scrolls
	EVT_SCROLL(Viewer::ActiveCtrlScr)
    END_EVENT_TABLE()
}

void MainFrame::UpdateViewers()
{
    for(dialog_list_t::iterator i = popups.begin(); i != popups.end(); i++) {
	Viewers::Viewer *d = static_cast<Viewers::Viewer *>(*i);
	if(d->auto_update)
	    d->Update();
    }
}

namespace Viewers
{
    IMPLEMENT_DYNAMIC_CLASS(DisList, wxPanel)

    DisList::DisList() :
	wxPanel(), tc(), sb(), didinit(false), nlines(0), issel(false),
	topaddr(0)
    {
    }

    void DisList::Refit(int cols)
    {
	if(!didinit) {
	    tc.Create(this, wxID_ANY, wxEmptyString, wxPoint(0, 0), wxDefaultSize,
		      wxTE_READONLY|wxTE_MULTILINE|wxTE_DONTWRAP|
		      wxTE_NOHIDESEL);
	    tc.Connect(wxEVT_LEFT_DOWN, wxMouseEventHandler(DisList::MouseEvent),
		       NULL, this);
	    tc.Connect(wxEVT_LEFT_UP, wxMouseEventHandler(DisList::MouseEvent),
		       NULL, this);
	    tc.Connect(wxEVT_MOTION, wxMouseEventHandler(DisList::MouseEvent),
		       NULL, this);
	    // FIXME: take tc out of tab order
	    sb.Create(this, wxID_ANY, wxDefaultPosition, wxDefaultSize,
		      wxSB_VERTICAL);
	    sb.SetScrollbar(0, 15, 500, 15);
	    didinit = true;
	}
	wxString line(wxT('M'), cols);
	// GetBestSize() in wxGTK is pretty damn worthless.  It's surprising
	// autosizing works at all.  wxGTK does not give good values
	// for wxTextCtrl (always 80xht_of_1liner)
	// Given this, we'll fake it using wxStaticText for lineheight
	// and add an extra line for extraheight
	// Just using GetTextExtent for lineheight might not work if extra
	// spacing is usually added between lines.
	wxStaticText st(this, wxID_ANY, line);
	wxSize winsz = st.GetBestSize();
	st.SetLabel(line + wxT('\n') + line);
	wxSize winsz2 = st.GetBestSize();
	lineheight = winsz2.GetHeight() - winsz.GetHeight();
	extraheight = winsz.GetHeight() /* - lineheight */;
	winsz.SetHeight(extraheight + 15 * lineheight);
	winsz.SetWidth(winsz.GetWidth() + sb.GetBestSize().GetWidth());
	SetMinSize(winsz);
	SetSize(winsz);
    }

    void DisList::MoveSB()
    {
	int pos;
	if(topaddr <= 100)
	    pos = topaddr;
	else if(topaddr >= maxaddr - 100)
	    pos = topaddr - maxaddr  + 500;
	else if(topaddr < 1100)
	    pos = (topaddr - 100) / 10 + 100;
	else if(topaddr >= maxaddr - 1100)
	    pos = (topaddr - maxaddr + 1100) / 10 + 300;
	else
	    pos = 250;
	int sz = nlines > 20 ? 20 : nlines;
	sb.SetScrollbar(pos, 20, 500, 20);
    }

    void DisList::MoveView(wxScrollEvent &ev)
    {
	int pos = ev.GetPosition();
	if(pos < 100)
	    topaddr = pos;
	else if(pos >= 400)
	    topaddr = maxaddr + pos - 500;
	else if(ev.GetEventType() == wxEVT_SCROLL_LINEUP) {
	    topaddr -= back_size;
	    MoveSB();
	} else if(ev.GetEventType() == wxEVT_SCROLL_LINEDOWN) {
	    topaddr = addrs[1];
	    MoveSB();
	} else if(ev.GetEventType() == wxEVT_SCROLL_PAGEUP) {
	    topaddr -= (nlines - 2) * back_size;
	    MoveSB();
	} else if(ev.GetEventType() == wxEVT_SCROLL_PAGEDOWN) {
	    topaddr = addrs[nlines - 2];
	    MoveSB();
	} else if(ev.GetEventType() == wxEVT_SCROLL_THUMBRELEASE) {
	    if(pos <= 200)
		topaddr = (pos - 100) * 10 + 100;
	    else if(pos >= 300)
		topaddr = (pos - 300) * 10 + maxaddr - 1100;
	    else
		// 200 .. 300 -> 1100 .. maxaddr - 1100
		topaddr = (pos - 200) * ((maxaddr - 2200) / 100) + 1100;
	    MoveSB();
	} // ignore THUMBTRACK and CHANGED
	RefillNeeded();
    }

    // generate event; due to ProcessEvent()'s nature, it will return only
    // when refill complete
    void DisList::RefillNeeded()
    {
	wxCommandEvent ev(EVT_REFILL_NEEDED, GetId());
	ev.SetEventObject(this);
	GetEventHandler()->ProcessEvent(ev);
    }

    // called by parent's refill handler or any other time strings have changed
    void DisList::Refill()
    {
	MoveSB();
	wxString val;
	for(int i = 0; i < nlines && i < strings.size(); i++) {
	    val += strings[i];
	    val += wxT('\n');
	}
	tc.SetValue(val);
	SetSel();
    }

    // on resize, recompute shown lines and refill if necessary
    void DisList::Resize(wxSizeEvent &ev)
    {
	if(!didinit)  // prevent crash on win32
	    return;
	wxSize sz = GetSize();
	int sbw = sb.GetSize().GetWidth();
	sz.SetWidth(sz.GetWidth() - sbw);
	sb.Move(sz.GetWidth(), 0);
	sb.SetSize(sbw, sz.GetHeight());
	nlines = (sz.GetHeight() + lineheight - 1) / lineheight;
	wxString val;
	tc.SetSize(sz.GetWidth(), (nlines + 1) * lineheight + extraheight);
	if(nlines > strings.size())
	    RefillNeeded();
	else {
	    for(int i = 0; i < nlines && i < strings.size(); i++) {
		val += strings[i];
		val += wxT('\n');
	    }
	    tc.SetValue(val);
	    SetSel();
	}
    }

    // highlight selected line, if visible
    void DisList::SetSel()
    {
	tc.SetSelection(0, 0);
	if(!issel)
	    return;
	if(nlines > addrs.size() || (u32)addrs[0] > seladdr ||
	   (u32)addrs[nlines - 1] <= seladdr)
	    return;
	for(int i = 0, start = 0; i < nlines; i++) {
	    if((u32)addrs[i + 1] > seladdr) {
		int end = start + strings[i].size() + 1;
		// on win32, wx helpfully inserts a CR before every LF
		// it also doesn't highlight the whole line, but that's
		// not critical
#ifdef __WXMSW__
		start += i;
		end += i + 1;
#endif
		tc.SetSelection(start, start + strings[i].size() + 1);
		return;
	    }
	    start += strings[i].size() + 1;
	}
    }

    void DisList::SetSel(u32 addr)
    {
	seladdr = addr;
	issel = true;
	if(addrs.size() < 4 || addrs.size() < nlines ||
	   topaddr > addr || (u32)addrs[addrs.size() - 4] < addr) {
	    topaddr = addr;
	    strings.clear();
	    addrs.clear();
	    RefillNeeded();
	} else
	    SetSel();
    }

    BEGIN_EVENT_TABLE(DisList, wxPanel)
	EVT_SIZE(DisList::Resize)
	EVT_SCROLL(DisList::MoveView)
    END_EVENT_TABLE()

    DEFINE_EVENT_TYPE(EVT_REFILL_NEEDED)

    IMPLEMENT_DYNAMIC_CLASS(MemView, wxPanel)

    MemView::MemView() :
	wxPanel(), disp(), sb(), didinit(false), nlines(0), selnib(-1),
	topaddr(0), addrlab(0)
    {
    }

    void MemView::Refit()
    {
	addrlen = maxaddr > 0xffff ? 8 : 4;
	if(!didinit) {
	    disp.Create(this, wxID_ANY, wxPoint(0, 0), wxDefaultSize,
			wxBORDER_NONE|wxWANTS_CHARS|wxTAB_TRAVERSAL);
	    disp.Connect(wxEVT_PAINT, wxPaintEventHandler(MemView::RepaintEv),
			 NULL, this);
	    disp.Connect(wxEVT_LEFT_DOWN, wxMouseEventHandler(MemView::MouseEvent),
			 NULL, this);
	    disp.Connect(wxEVT_MOTION, wxMouseEventHandler(MemView::MouseEvent),
			 NULL, this);
	    disp.Connect(wxEVT_LEFT_UP, wxMouseEventHandler(MemView::MouseEvent),
			 NULL, this);
	    disp.Connect(wxEVT_CHAR, wxKeyEventHandler(MemView::KeyEvent),
			 NULL, this);
	    disp.SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_LISTBOX));
	    sb.Create(this, wxID_ANY, wxDefaultPosition, wxDefaultSize,
		      wxSB_VERTICAL);
	    sb.SetScrollbar(0, 15, 500, 15);
	}
	wxClientDC dc(&disp);
	// doesn't seem to inherit font properly
	dc.SetFont(GetFont());
	dc.GetTextExtent(wxT('M'), &charwidth, &charheight);
	if(!didinit) {
	    // using 2-phase init doesn't seem to work
	    caret = new wxCaret(&disp, charwidth, charheight);
	    caret->Hide();
	    disp.SetCaret(caret);
	    didinit = true;
	}
	wxSize sz(charwidth * (69 + addrlen) + sb.GetBestSize().GetWidth(),
		  charheight * 15);
	sz = sz + GetSize() - GetClientSize();
	SetMinSize(sz);
	SetSize(sz);
    }

    void MemView::MouseEvent(wxMouseEvent &ev)
    {
	if(ev.GetEventType() == wxEVT_MOTION && !ev.LeftIsDown())
	    return;
	int x = ev.GetX() / charwidth, y = ev.GetY() / charheight;
	x -= addrlen + 3;
	if(x < 0 || y < 0 || y > nlines)
	    return;
	int word, nib;
	int nnib = 2 << fmt, nword = 16 >> fmt;
	int preasc = (nnib + 1) * nword + 2;
	isasc = x >= preasc;
	if(isasc) {
	    word = (x - preasc) * 2 / nnib;
	    nib = (x - preasc) * 2 % nnib;
	} else {
	    word = x / (nnib + 1);
	    nib = x % (nnib + 1);
	    nib = nnib - nib - 1;
	}
	if(nib < 0 || word >= nword)
	    return;
	seladdr = topaddr + y * 16;
	selnib = word * nnib + nib;
	Show(seladdr);
    }

    void MemView::ShowCaret()
    {
	if(seladdr < topaddr || seladdr >= topaddr + nlines * 16)
	    selnib = -1;
	if(selnib < 0) {
	    while(caret->IsVisible())
		caret->Hide();
	    if(addrlab)
		addrlab->SetLabel(wxEmptyString);
	    return;
	}
	if(addrlab) {
	    wxString lab;
	    u32 addr = seladdr + selnib / 2;
	    if(!isasc)
		addr &= ~((1 << fmt) - 1);
	    lab.Printf(addrlen == 8 ? wxT("0x%08X") : wxT("0x%04X"), addr);
	    addrlab->SetLabel(lab);
	}
	int y = (seladdr - topaddr) / 16;
	int x = addrlen + 3;
	int nnib = 2 << fmt, nword = 16 >> fmt;
	if(isasc)
	    x += (nnib + 1) * nword + 2 + selnib / 2;
	else
	    x += (nnib + 1) * (selnib / nnib) + nnib - selnib % nnib - 1;
	caret->Move(x * charwidth, y * charheight);
	while(!caret->IsVisible())
	    caret->Show();
	disp.SetFocus();
    }

    void MemView::KeyEvent(wxKeyEvent &ev)
    {
	u32 k = ev.GetKeyCode();
	int nnib = 2 << fmt;
	switch(k) {
	case WXK_RIGHT:
	case WXK_NUMPAD_RIGHT:
	    if(isasc)
		selnib += 2;
	    else if(ev.GetModifiers() == wxMOD_SHIFT)
		selnib += 2 << fmt;
	    else if(!(selnib % nnib))
		selnib += nnib + nnib - 1;
	    else
		selnib--;
	    if(selnib >= 32) {
		if(seladdr == maxaddr - 16)
		    selnib = 32 - nnib;
		else {
		    selnib -= 32;
		    seladdr += 16;
		}
	    }
	    break;
	case WXK_LEFT:
	case WXK_NUMPAD_LEFT:
	    if(isasc)
		selnib -= 2;
	    else if(ev.GetModifiers() == wxMOD_SHIFT)
		selnib -= 2 << fmt;
	    else if(!(++selnib % nnib))
		selnib -= nnib * 2;
	    if(selnib < 0) {
		if(!seladdr)
		    selnib = nnib - 1;
		else {
		    selnib += 32;
		    seladdr -= 16;
		}
	    }
	    break;
	case WXK_DOWN:
	case WXK_NUMPAD_DOWN:
	    if(seladdr < maxaddr - 16)
		seladdr += 16;
	    break;
	case WXK_UP:
	case WXK_NUMPAD_UP:
	    if(seladdr > 0)
		seladdr -= 16;
	    break;
	default:
	    if(k > 0x7f ||
	       (isasc && !isprint(k)) || (!isasc && !isxdigit(k))) {
		ev.Skip();
		return;
	    }

	    // location in data array
	    int wno = (seladdr - topaddr) / 4 + selnib / 8;
	    int bno = (selnib % 8) / 2;
	    int nibno = selnib % 2;

	    // now that selnib/seladdr isn't needed any more, advance pointer
	    if(isasc)
		selnib += 2;
	    else if(!(selnib % nnib))
		selnib += nnib + nnib - 1;
	    else
		selnib--;
	    if(selnib >= 32) {
		if(seladdr == maxaddr - 16)
		    selnib = 32 - nnib;
		else {
		    selnib -= 32;
		    seladdr += 16;
		}
	    }

	    u32 mask, val;
	    if(isasc) {
		mask = 0xff << bno * 8;
		val = k << bno * 8;
	    } else {
		mask = 0xf << bno * 8 + nibno * 4;
		val = isdigit(k) ? k - '0' : tolower(k) + 10 - 'a';
		val <<= bno * 8 + nibno * 4;
	    }
	    if((words[wno] & mask) == val)
		break;
	    words[wno] = words[wno] & ~mask | val;
	    writeaddr = topaddr + 4 * wno;
	    val = words[wno];
	    switch(fmt) {
	    case 0:
		writeval = (val >> bno * 8) & 0xff;
		writeaddr += bno;
		break;
	    case 1:
		writeval = (val >> (bno / 2) * 16) & 0xffff;
		writeaddr += bno & ~1;
		break;
	    case 2:
		writeval = val;
		break;
	    }
	    // write value; this will not return until value has been written
	    wxCommandEvent ev(EVT_WRITEVAL, GetId());
	    ev.SetEventObject(this);
	    GetEventHandler()->ProcessEvent(ev);
	    // now refresh whole screen.  Really need to make this more
	    // efficient some day
	    Repaint();
	}
	Show(seladdr);
    }

    void MemView::MoveSB()
    {
	int pos;
	if(topaddr / 16 <= 100)
	    pos = topaddr / 16;
	else if(topaddr / 16 >= maxaddr / 16 - 100)
	    pos = topaddr / 16 - maxaddr / 16 + 500;
	else if(topaddr / 16 < 1100)
	    pos = (topaddr / 16 - 100) / 10 + 100;
	else if(topaddr / 16 >= maxaddr / 16 - 1100)
	    pos = (topaddr / 16 - maxaddr / 16 + 1100) / 10 + 300;
	else
	    pos = 250;
	int sz = nlines > 20 ? 20 : nlines;
	sb.SetScrollbar(pos, 20, 500, 20);
    }

    void MemView::MoveView(wxScrollEvent &ev)
    {
	int pos = ev.GetPosition();
	if(pos < 100)
	    topaddr = pos * 16;
	else if(pos >= 400)
	    topaddr = maxaddr + (pos - 500) * 16;
	else if(ev.GetEventType() == wxEVT_SCROLL_LINEUP) {
	    topaddr -= 16;
	    MoveSB();
	} else if(ev.GetEventType() == wxEVT_SCROLL_LINEDOWN) {
	    topaddr += 16;
	    MoveSB();
	} else if(ev.GetEventType() == wxEVT_SCROLL_PAGEUP) {
	    topaddr -= (nlines - 2) * 16;
	    MoveSB();
	} else if(ev.GetEventType() == wxEVT_SCROLL_PAGEDOWN) {
	    topaddr += (nlines - 2) * 16;
	    MoveSB();
	} else if(ev.GetEventType() == wxEVT_SCROLL_THUMBRELEASE) {
	    if(pos <= 200)
		topaddr = ((pos - 100) * 10 + 100) * 16;
	    else if(pos >= 300)
		topaddr = ((pos - 300) * 10 - 1100) * 16 + maxaddr;
	    else
		topaddr = ((pos - 200) * ((maxaddr / 16 - 2200) / 100) + 1100) * 16;
	    MoveSB();
	} // ignore THUMBTRACK and CHANGED
	RefillNeeded();
    }

    // generate event; due to ProcessEvent()'s nature, it will return only
    // when refill complete
    void MemView::RefillNeeded()
    {
	wxCommandEvent ev(EVT_REFILL_NEEDED, GetId());
	ev.SetEventObject(this);
	GetEventHandler()->ProcessEvent(ev);
    }

    // FIXME:  following 3 repainters do not work as intended: there is
    // more flickering than necessary and yet still garbage on screen sometimes
    // (probably in part from wxCaret)

    // called by parent's refill handler or any other time strings have changed
    void MemView::Refill()
    {
	MoveSB();
	wxClientDC dc(&disp);
	dc.Clear();
	Refill(dc);
	ShowCaret();
    }

    void MemView::Repaint()
    {
	wxClientDC dc(&disp);
	dc.SetBackgroundMode(wxSOLID);
	Refill(dc);
    }

    void MemView::RepaintEv(wxPaintEvent &ev)
    {
	wxPaintDC dc(&disp);
	dc.SetBackgroundMode(wxSOLID);
	Refill(dc);
	ShowCaret();
    }

    void MemView::Refill(wxDC &dc)
    {
	// don't want caret drawing at same time due to timer event
	wxCaretSuspend cs(&disp);
	// doesn't seem to inherit font properly
	dc.SetFont(GetFont());
	for(int i = 0; i < nlines && i < words.size() / 4; i++) {
	    wxString line, word;
	    line.Printf(maxaddr > 0xffff ? wxT("%08X   ") : wxT("%04X   "), topaddr + i * 16);
	    for(int j = 0; j < 4; j++) {
		u32 v = words[i * 4 + j];
		switch(fmt) {
		case 0:
		    word.Printf(wxT("%02X %02X %02X %02X "),
				v & 0xff, (v >> 8) & 0xff, (v >> 16) & 0xff,
				(v >> 24) & 0xff);
		    break;
		case 1:
		    word.Printf(wxT("%04X %04X "), v & 0xffff, (v >> 16) & 0xffff);
		    break;
		case 2:
		    word.Printf(wxT("%08X "), v);
		    break;
		}
		line.append(word);
	    }
	    line.append(wxT("  "));
	    for(int j = 0; j < 4; j++) {
		u32 v = words[i * 4 + j];
#define appendc(c) line.append(isascii((c) & 0xff) && isprint((c) & 0xff) ? \
    (wxChar)((c) & 0xff) : wxT('.'))
		appendc(v);
		appendc(v >> 8);
		appendc(v >> 16);
		appendc(v >> 24);
	    }
	    dc.DrawText(line, 0, i * charheight);
	}
	int lloc = charwidth * ((addrlen + 1) * 2 + 1) / 2;
	dc.DrawLine(lloc, 0, lloc, nlines * charheight);
	lloc = charwidth * (2 * (addrlen + 3 + 32 + 4 +
			    (fmt == 0 ? 3 * 4 : fmt == 1 ? 4 : 0)) + 1) / 2;
	dc.DrawLine(lloc, 0, lloc, nlines * charheight);
    }

    // on resize, recompute shown lines and refill if necessary
    void MemView::Resize(wxSizeEvent &ev)
    {
	if(!didinit)  // prevent crash on win32
	    return;
	wxSize sz = GetClientSize();
	int sbw = sb.GetSize().GetWidth();
	sz.SetWidth(sz.GetWidth() - sbw);
	sb.Move(sz.GetWidth(), 0);
	sb.SetSize(sbw, sz.GetHeight());
	nlines = (sz.GetHeight() + charheight - 1) / charheight;
	wxString val;
	disp.SetSize(sz.GetWidth(), (nlines + 1) * charheight);
	if(nlines > words.size() / 4) {
	    if(topaddr + nlines * 16 > maxaddr)
		topaddr = maxaddr - nlines * 16 + 1;
	    RefillNeeded();
	} else
	    Refill();
    }

    void MemView::Show(u32 addr, bool force_update)
    {
	if(addr < topaddr || addr >= topaddr + (nlines - 1) * 16) {
	    // align to nearest 16-byte block
	    // note that mfc interface only aligns to nearest (1<<fmt)-byte
	    u32 newtopaddr = addr & ~0xf;
	    if(newtopaddr + nlines * 16 > maxaddr)
		newtopaddr = maxaddr - nlines * 16 + 1;
	    force_update = newtopaddr != topaddr;
	    topaddr = newtopaddr;
	}
	if(force_update) {
	    words.clear();
	    RefillNeeded();
	} else
	    ShowCaret();
    }

    u32 MemView::GetAddr()
    {
	if(selnib < 0)
	    return topaddr;
	else
	    return seladdr + (selnib / 2 & ~((1 << fmt) - 1));
    }

    BEGIN_EVENT_TABLE(MemView, wxPanel)
	EVT_SIZE(MemView::Resize)
	EVT_SCROLL(MemView::MoveView)
    END_EVENT_TABLE()

    DEFINE_EVENT_TYPE(EVT_WRITEVAL)

    ColorView::ColorView(wxWindow *parent, wxWindowID id)
	// default for MSW appears to be BORDER_SUNKEN
        : wxControl(parent, id, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE),
	  r(0), g(0), b(0)
    {
	wxBoxSizer *sz = new wxBoxSizer(wxHORIZONTAL);
	cp = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(75, 75),
			 wxBORDER_SUNKEN);
	sz->Add(cp);
	wxGridSizer *gs = new wxGridSizer(2);
	wxStaticText *lab = new wxStaticText(this, wxID_ANY, _("R:"));
	gs->Add(lab, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5);
	rt = new wxStaticText(this, wxID_ANY, wxT("255"), wxDefaultPosition,
			      wxDefaultSize, wxST_NO_AUTORESIZE);
	gs->Add(rt, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5);
	lab = new wxStaticText(this, wxID_ANY, _("G:"));
	gs->Add(lab, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5);
	gt = new wxStaticText(this, wxID_ANY, wxT("255"), wxDefaultPosition,
			      wxDefaultSize, wxST_NO_AUTORESIZE);
	gs->Add(gt, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5);
	lab = new wxStaticText(this, wxID_ANY, _("B:"));
	gs->Add(lab, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5);
	bt = new wxStaticText(this, wxID_ANY, wxT("255"), wxDefaultPosition,
			      wxDefaultSize, wxST_NO_AUTORESIZE);
	gs->Add(bt, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5);
	sz->Add(gs);
	sz->Layout();
	SetSizerAndFit(sz);
	SetRGB(-1, -1, -1);
    }

    void ColorView::SetRGB(int r, int g, int b)
    {
	if(r == -1 || g == -1 || b == -1) {
	    cp->SetBackgroundColour(wxNullColour);
	    rt->SetLabel(wxT(""));
	    gt->SetLabel(wxT(""));
	    bt->SetLabel(wxT(""));
	    return;
	}
	cp->SetBackgroundColour(wxColour(r, g, b));
	wxString s;
	// FIXME: make shift an option; currently hard-coded to rgb555
	s.Printf(wxT("%d"), r >> 3);
	rt->SetLabel(s);
	s.Printf(wxT("%d"), g >> 3);
	gt->SetLabel(s);
	s.Printf(wxT("%d"), b >> 3);
	bt->SetLabel(s);
    }

    IMPLEMENT_DYNAMIC_CLASS(PixView, wxPanel);

    BEGIN_EVENT_TABLE(PixView, wxPanel)
	EVT_PAINT(PixView::Redraw)
	EVT_LEFT_UP(PixView::SelPoint)
    END_EVENT_TABLE()

    bool PixView::InitBMP(int w, int h, ColorView *cv)
    {
	im = wxImage(w, h);
	bm = new wxBitmap(im);
	if(!bm)
	    return false;
	selx = sely = -1;
	cview = cv;
	return true;
    }

    void PixView::SetData(const unsigned char *data, int stride, int x, int y)
    {
	if(!bm)
	    return;
	ox = x;
	oy = y;
	if(!data) {
	    im.SetRGB(wxRect(0, 0, im.GetWidth(), im.GetHeight()), 0, 0, 0);
	    selx = sely = -1;
	} else {
	    data += (y * stride + x) * 3;
	    for(y = 0; y < im.GetHeight(); y++) {
		for(x = 0; x < im.GetWidth(); x++) {
		    im.SetRGB(x, y, *data, data[1], data[2]);
		    data += 3;
		}
		data += 3 * (stride - x);
	    }
	}
	delete bm;
	bm = new wxBitmap(im);
	if(selx >= 0 && cview)
	    cview->SetRGB(im.GetRed(selx, sely), im.GetGreen(selx, sely),
			  im.GetBlue(selx, sely));
	else if(cview)
	    cview->SetRGB(-1, -1, -1);
	Refresh();
    }

    void PixView::SetSel(int x, int y, bool desel_cview_update)
    {
	if(x >= ox && y >= oy && x - ox < im.GetWidth() && y - oy < im.GetHeight()) {
	    int oselx = selx, osely = sely;
	    selx = x - ox;
	    sely = y - oy;
	    if(selx != oselx || sely != osely) {
		if(cview)
		    cview->SetRGB(im.GetRed(selx, sely), im.GetGreen(selx, sely),
				  im.GetBlue(selx, sely));
		Refresh();
	    }
	} else {
	    bool r = selx >= 0 && sely >= 0;
	    selx = sely = -1;
	    if(r) {
		if(cview && desel_cview_update)
		    cview->SetRGB(-1, -1, -1);
		Refresh();
	    }
	}
    }

    void PixView::Redraw(wxPaintEvent &ev)
    {
	if(!bm)
	    return;
	wxPaintDC dc(this);
	double sx, sy;
	int w, h;
	GetClientSize(&w, &h);
	sx = (double)w / im.GetWidth();
	sy = (double)h / im.GetHeight();
	dc.SetUserScale(sx, sy);
	dc.DrawBitmap(*bm, 0, 0, false);
	dc.SetUserScale(sx / 8, sy / 8);
	// grid color is hard-coded to gray
	dc.SetPen(*wxGREY_PEN);
	// grid is only on top/left, just like win32 port
	for(int y = 0; y < im.GetHeight(); y++)
	    dc.DrawLine(0, y * 8, im.GetWidth() * 8, y * 8);
	for(int x = 0; x < im.GetWidth(); x++)
	    dc.DrawLine(x * 8, 0, x * 8, im.GetHeight() * 8);
	if(selx >= 0) {
	    // sel color is hard-coded to red
	    dc.SetPen(*wxRED_PEN);
	    dc.SetBrush(*wxTRANSPARENT_BRUSH);
	    dc.DrawRectangle(selx * 8, sely * 8, 8, 8);
	}
    }

    void PixView::SelPoint(wxMouseEvent &ev)
    {
	if(!bm)
	    return;
	int w, h;
	GetClientSize(&w, &h);
	wxPoint p = ev.GetPosition();
	if(p.x < 0 || p.x >= w || p.y < 0 || p.y >= h) {
	    bool r = selx >= 0 && sely >= 0;
	    selx = sely = -1;
	    if(r) {
		Refresh();
		if(cview)
		    cview->SetRGB(-1, -1, -1);
	    }
	    return;
	}
	int oselx = selx, osely = sely;
	selx = p.x * im.GetWidth() / w;
	sely = p.y * im.GetHeight() / h;
	if(selx != oselx || sely != osely) {
	    Refresh();
	    if(cview)
		cview->SetRGB(im.GetRed(selx, sely), im.GetGreen(selx, sely),
			      im.GetBlue(selx, sely));
	}
    }

    IMPLEMENT_DYNAMIC_CLASS(PixViewEvt, PixView)
    void PixViewEvt::SetData(const unsigned char *data, int stride, int x,
			     int y)
    {
	PixView::SetData(data, stride, x, y);
	if(selx >= 0 && sely >= 0)
	    click();
    }

    void PixViewEvt::SelPoint(wxMouseEvent &ev)
    {
	PixView::SelPoint(ev);
	click();
    }

    void PixViewEvt::click()
    {
	wxMouseEvent ev;
	ev.SetEventType(EVT_COMMAND_GFX_CLICK);
	ev.ResumePropagation(wxEVENT_PROPAGATE_MAX);
	ev.SetEventObject(this);
	ev.SetId(GetId());
	//ev.SetX(selx);
	ev.m_x = selx;
	//ev.SetY(sely);
	ev.m_y = sely;
	GetEventHandler()->ProcessEvent(ev);
    }

    IMPLEMENT_DYNAMIC_CLASS(GfxPanel, wxPanel)
    void GfxPanel::DrawBitmap(wxPaintEvent &ev)
    {
	if(!bm)
	    return;
	wxPaintDC dc(this);
	wxSize sz = GetClientSize();
	double scalex = (double)sz.GetWidth() / bmw;
	double scaley = (double)sz.GetHeight() / bmh;
	dc.SetUserScale(scalex, scaley);
	dc.DrawBitmap(*bm, 0, 0);
	if(selx >= 0) {
	    if(selx > bmw - 4 || sely > bmh - 4)
		pv->SetData(NULL, 0, 0, 0);
	    else
		pv->SetData(im->GetData(), im->GetWidth(), selx - 4, sely - 4);
	}
    }

    void GfxPanel::Click(wxMouseEvent &ev)
    {
	DoSel(ev, ev.GetEventType() == wxEVT_LEFT_DOWN);
    }

    void GfxPanel::MouseMove(wxMouseEvent &ev)
    {
	if(!ev.LeftIsDown()) {
	    ev.Skip();
	    return;
	}
	DoSel(ev);
    }

    void GfxPanel::DoSel(wxMouseEvent &ev, bool force)
    {
	if(!bm)
	    return;
	int x = ev.GetX(), y = ev.GetY();
	if(x < 0 || y < 0)
	    return;
	wxSize sz = GetClientSize();
	if(x > sz.GetWidth() || y > sz.GetHeight())
	    return;
	x = x * bmw / sz.GetWidth();
	y = y * bmh / sz.GetHeight();
	wxMouseEvent ev2(ev);
	ev2.SetEventType(EVT_COMMAND_GFX_CLICK);
	ev2.ResumePropagation(wxEVENT_PROPAGATE_MAX);
	ev2.SetEventObject(this);
	ev2.SetId(GetId());
	//ev2.SetX(x);
	ev2.m_x = x;
	//ev2.SetY(y);
	ev2.m_y = y;
	GetEventHandler()->ProcessEvent(ev2);
	if(x < 4)
	    x = 4;
	else if(x > bmw - 4)
	    x = bmw - 4;
	if(y < 4)
	    y = 4;
	else if(y > bmh - 4)
	    y = bmh - 4;
	if(force || selx != x || sely != y) {
	    selx = x;
	    sely = y;
	    pv->SetData(im->GetData(), im->GetWidth(), selx - 4, sely - 4);
	}
    }

    DEFINE_EVENT_TYPE(EVT_COMMAND_GFX_CLICK)

    BEGIN_EVENT_TABLE(GfxPanel, wxPanel)
	EVT_PAINT(GfxPanel::DrawBitmap)
	EVT_LEFT_DOWN(GfxPanel::Click)
	EVT_LEFT_UP(GfxPanel::Click)
	EVT_MOTION(GfxPanel::MouseMove)
    END_EVENT_TABLE()

    GfxViewer::GfxViewer(const wxChar *dname, int maxw, int maxh) :
	Viewer(dname), image(maxw, maxh)
    {
	gv = XRCCTRL(*this, "GfxView", GfxPanel);
	if(!gv || !(gvs = dynamic_cast<wxScrolledWindow *>(gv->GetParent())))
	    baddialog();
	gvs->SetMinSize(gvs->GetSize());
	gvs->SetScrollRate(1, 1);
	gv->SetSize(maxw, maxh);
	gvs->SetVirtualSize(maxw, maxh);
	gv->im = &image;
	gv->bmw = maxw;
	gv->bmh = maxh;
	ColorView *cv;
	colorctrl(cv, "Color");
	pixview(gv->pv, "Zoom", 8, 8, cv);
	str = XRCCTRL(*this, "Stretch", wxCheckBox);
	if(!str)
	    baddialog();
    }

    void GfxViewer::ChangeBMP()
    {
	delete gv->bm;
	gv->bm = new wxBitmap(image);
	gv->Refresh();
    }

    void GfxViewer::BMPSize(int w, int h)
    {
	if(gv->bmw != w || gv->bmh != h) {
	    gv->bmw = w;
	    gv->bmh = h;
	    if(!str->GetValue()) {
		gv->SetSize(w, h);
		gvs->SetVirtualSize(gv->GetSize());
	    }
	}
    }

    void GfxViewer::StretchTog(wxCommandEvent &ev)
    {
	wxSize sz;
	if(str->GetValue()) {
	    // first time to remove scrollbars
	    gvs->SetVirtualSize(gvs->GetClientSize());
	    // second time to expand to outer edges
	    sz = gvs->GetClientSize();
	} else
	    sz = wxSize(gv->bmw, gv->bmh);
	gv->SetSize(sz);
	gvs->SetVirtualSize(sz);
    }

    void GfxViewer::SaveBMP(wxCommandEvent &ev)
    {
	GameArea *panel = wxGetApp().frame->GetPanel();
	if(bmp_save_dir.empty()) {
	    bmp_save_dir = gopts.scrshot_dir;
	    if(bmp_save_dir.size()) {
		wxFileName sp(bmp_save_dir, wxEmptyString);
		if(!sp.IsAbsolute())
		    bmp_save_dir = panel->game_dir() + wxT('/') + gopts.scrshot_dir;
	    }
	}
	// no attempt is made here to translate the dialog type name
	// it's just a suggested name, anyway
	wxString def_name = panel->game_name() + wxT('-') + dname;
	def_name.resize(def_name.size() - 6); // strlen("Viewer")
	if(gopts.cap_format)
	    def_name += wxT(".bmp");
	else
	    def_name += wxT(".png");
	wxFileDialog dlg(GetGrandParent(), _("Select output file"), bmp_save_dir, def_name,
			 _("PNG images|*.png|BMP images|*.bmp"), wxFD_SAVE|wxFD_OVERWRITE_PROMPT);
	dlg.SetFilterIndex(gopts.cap_format);
	int ret = dlg.ShowModal();
	bmp_save_dir = dlg.GetDirectory();
	if(ret != wxID_OK)
	    return;
	wxBitmap obmp = gv->bm->GetSubBitmap(wxRect(0, 0, gv->bmh, gv->bmh));
	wxString fn = dlg.GetPath();
	wxBitmapType fmt = dlg.GetFilterIndex() ? wxBITMAP_TYPE_BMP : wxBITMAP_TYPE_PNG;
	if(fn.size() > 4) {
	    if(wxString(fn.substr(fn.size() - 4)).IsSameAs(wxT(".bmp"), false))
		fmt = wxBITMAP_TYPE_BMP;
	    else if(wxString(fn.substr(fn.size() - 4)).IsSameAs(wxT(".png"), false))
		fmt = wxBITMAP_TYPE_PNG;
	}
	obmp.SaveFile(fn, fmt);
    }

    void GfxViewer::RefreshEv(wxCommandEvent &ev)
    {
	Update();
    }

    wxString GfxViewer::bmp_save_dir = wxEmptyString;

    BEGIN_EVENT_TABLE(GfxViewer, Viewer)
	EVT_CHECKBOX(XRCID("Stretch"), GfxViewer::StretchTog)
	EVT_BUTTON(XRCID("Refresh"), GfxViewer::RefreshEv)
	EVT_BUTTON(XRCID("Save"), GfxViewer::SaveBMP)
    END_EVENT_TABLE()

    IMPLEMENT_DYNAMIC_CLASS(DispCheckBox, wxCheckBox)

    BEGIN_EVENT_TABLE(DispCheckBox, wxCheckBox)
	EVT_LEFT_DOWN(DispCheckBox::MouseEvent)
	EVT_LEFT_UP(DispCheckBox::MouseEvent)
    END_EVENT_TABLE()
}
