If you have comments or questions concerning this source file, discuss them in the forum.
/*
Copyright (c) 2002 Nicolai Haehnle

See the license.txt for details. If that file was not included in the
source distributions, please email <prefect@rtts.org>
*/
// ed_main.cpp - the map editor

#include "editor.h"

#include "rttspanels.h"

#include "gui_panel.h"
#include "gui_grid.h"
#include "gui_inputbox.h"
#include "gui_listbox.h"
#include "gui_radiobutton.h"
#include "gui_checkbox.h"
#include "gui_popupmenu.h"


/*
==============================================================================

PickUnitDlg IMPLEMENTATION

==============================================================================
*/

class PickUnitDlg : public RttsDialog {
public:
    GListBox        *m_pListBox;
    GLabel          *m_pSprite;
    GButton         *m_pOk;

    int             m_count;
    airunit_type_t  **m_types;

public:
    PickUnitDlg(GPanel *pParent, int count, airunit_type_t **types);

    void Refresh();
};

PickUnitDlg::PickUnitDlg(GPanel *pParent, int count, airunit_type_t **types)
    : RttsDialog(pParent, 50, 50, 540, 380)
{
    GButton *pBtn;
    int i;

    m_count = count;
    m_types = types;

    m_pListBox = new GListBox(this, 30, 30, 300, 320);
    m_pListBox->SetColor(192, 192, 192);
    m_pListBox->SetSelectedColor(255, 255, 0);

    for(i = 0; i < count; i++)
        m_pListBox->AddString(types[i]->name);

    m_pListBox->SelectChanged.Connect(this, &PickUnitDlg::Refresh);

    m_pOk = new RttsButton(this, 390, 30, 120, 30, "OK", valign_center|halign_center);
    m_pOk->Clicked.Connect(this, &PickUnitDlg::EndModal);

    pBtn = new RttsButton(this, 390, 60, 120, 30, "CANCEL",
                        valign_center|halign_center, -1);
    pBtn->Clicked.Connect(this, &PickUnitDlg::EndModal);

    m_pSprite = new GLabel(this, 330, 100, 200, 260, 0, valign_center|halign_center);

    Refresh();
}

void PickUnitDlg::Refresh()
{
    int id;

    id = m_pListBox->GetSelectionId();
    if (id >= 0) {
        m_pSprite->SetSprite(m_types[id]->sprite);
        m_pOk->SetOblivious(false);
        m_pOk->SetId(id);
    } else
        m_pOk->SetOblivious(true);
}

/*
==============================================================================

SettingsDlg IMPLEMENTATION

==============================================================================
*/

class SettingsDlg : public RttsDialog {
public:
    GInputBox       *m_pLevel;
    GInputBox       *m_pName;
    GInputBox       *m_pAuthor;

    GButton         *m_pOk;

public:
    SettingsDlg(GPanel *pParent);

    void Refresh();
    void Ok(int id);

    void PokeLevel(int level);
    int PeekLevel();
    void PokeName(const char *name);
    const char *PeekName();
    void PokeAuthor(const char *name);
    const char *PeekAuthor();
};

SettingsDlg::SettingsDlg(GPanel *pParent)
    : RttsDialog(pParent, 100, 100, 440, 280)
{
    GLabel *pLabel;
    GButton *pBtn;

    pLabel = new GLabel(this, 30, 34, 440, 16, "Level:", 0);
    pLabel->SetColor(192, 32, 0);
    pLabel->AdjustSize();

    m_pLevel = new GInputBox(this, 150, 30, 100, 24, 0, valign_center);
    m_pLevel->SetColor(192, 192, 192);
    m_pLevel->SetMaxLen(4);
    m_pLevel->SetModeInteger(1, 9999);
    m_pLevel->Changed.Connect(this, &SettingsDlg::Refresh);

    pLabel = new GLabel(this, 30, 70, 440, 16, "Level name:", 0);
    pLabel->SetColor(192, 32, 0);
    pLabel->AdjustSize();

    m_pName = new GInputBox(this, 30, 100, 380, 24, 0, valign_center);
    m_pName->SetColor(192, 192, 192);
    m_pName->SetMaxLen(31);
    m_pName->Changed.Connect(this, &SettingsDlg::Refresh);

    pLabel = new GLabel(this, 30, 130, 440, 16, "Author:", 0);
    pLabel->SetColor(192, 32, 0);
    pLabel->AdjustSize();

    m_pAuthor = new GInputBox(this, 30, 160, 380, 24, 0, valign_center);
    m_pAuthor->SetColor(192, 192, 192);
    m_pAuthor->SetMaxLen(31);
    m_pAuthor->Changed.Connect(this, &SettingsDlg::Refresh);

    m_pOk = new RttsButton(this, 30, 225, 200, 25, "OK", valign_center, 1);
    m_pOk->Clicked.Connect(this, &SettingsDlg::Ok);

    SetDefaultButton(m_pOk);

    pBtn = new RttsButton(this, 210, 225, 200, 25, "CANCEL", valign_center|halign_right, -1);
    pBtn->Clicked.Connect(this, &SettingsDlg::EndModal);

    Refresh();
}

void SettingsDlg::Refresh()
{
    bool valid;

    valid = true;
    if (!m_pLevel->IsValid())
        valid = false;
    if (!m_pName->GetTextLength())
        valid = false;
    if (!m_pAuthor->GetTextLength())
        valid = false;

    m_pOk->SetOblivious(!valid);
}

void SettingsDlg::Ok(int id)
{
    char buf[MAX_OSPATH];
    int level;

    level = m_pLevel->GetInteger();
    snprintf(buf, sizeof(buf), "levels/%04i.%s.lvl", level, m_pName->GetText());

    if (L_FileExists(buf)) {
        snprintf(buf, sizeof(buf), "Level %04i - %s exists. Continue anyway?",
            level, m_pName->GetText());
        if (!RttsMessageBox::YesNo(this, buf))
            return;
    }

    EndModal(1);
}

void SettingsDlg::PokeLevel(int level)
{
    char buf[16];

    snprintf(buf, sizeof(buf), "%i", level+1);
    m_pLevel->SetText(buf);

    Refresh();
}

int SettingsDlg::PeekLevel()
{
    return m_pLevel->GetInteger() - 1;
}

void SettingsDlg::PokeName(const char *name)
{
    m_pName->SetText(name);
    Refresh();
}

const char *SettingsDlg::PeekName()
{
    return m_pName->GetText();
}

void SettingsDlg::PokeAuthor(const char *name)
{
    m_pAuthor->SetText(name);
    Refresh();
}

const char *SettingsDlg::PeekAuthor()
{
    return m_pAuthor->GetText();
}

/*
==============================================================================

LoadDlg IMPLEMENTATION

==============================================================================
*/

class LoadDlg : public RttsDialog {
public:
    GListBox    *m_pListBox;
    GButton     *m_pOk;

public:
    LoadDlg(GPanel *pParent);

    void Refresh();

    const char *PeekFilename();
};

LoadDlg::LoadDlg(GPanel *pParent)
    : RttsDialog(pParent, 100, 50, 440, 380)
{
    GButton *pBtn;
    char **files;
    int count, i;

    m_pListBox = new GListBox(this, 30, 30, 380, 290);
    m_pListBox->SetColor(192, 192, 192);
    m_pListBox->SetSelectedColor(255, 255, 0);

    count = L_FindFiles("levels", "*.lvl", &files);

    for(i = 0; i < count; i++)
        m_pListBox->AddString(files[i]);

    L_FreeFindFiles(files);

    m_pListBox->SelectChanged.Connect(this, &LoadDlg::Refresh);

    m_pOk = new RttsButton(this, 30, 330, 0, 30, "OK", valign_center, 1);
    m_pOk->Clicked.Connect(this, &LoadDlg::EndModal);

    SetDefaultButton(m_pOk);


    pBtn = new RttsButton(this, 350, 330, 0, 30, "CANCEL", valign_center|halign_right, -1);
    pBtn->Clicked.Connect(this, &LoadDlg::EndModal);

    Refresh();
}

void LoadDlg::Refresh()
{
    bool valid;

    valid = true;
    if (!m_pListBox->GetSelection())
        valid = false;

    m_pOk->SetOblivious(!valid);
}

const char *LoadDlg::PeekFilename()
{
    return m_pListBox->GetSelection();
}

/*
==============================================================================

ResizeDlg IMPLEMENTATION

==============================================================================
*/

class ResizeDlg : public RttsDialog {
public:
    GRadioButtonGroup   m_remove;
    GRadioButtonGroup   m_start;
    GInputBox           *m_pLines;

public:
    ResizeDlg(GPanel *pParent);

    void Ok(int id);

    int PeekLines() { return m_pLines->GetInteger(); }
    bool PeekRemove() { return m_remove.Get() ? true : false; }
    bool PeekStart() { return m_start.Get() ? true : false; }
};

ResizeDlg::ResizeDlg(GPanel *pParent)
    : RttsDialog(pParent, 100, 100, 440, 280)
{
    GLabel *pLabel;
    GRadioButton *radio;
    GButton *btn;

    pLabel = new GLabel(this, 30, 30, 200, 24, "# of lines:", valign_center);

    m_pLines = new GInputBox(this, 230, 30, 150, 24, "0", valign_center);
    m_pLines->SetModeInteger(0, 9999);

    // Remove group
    radio = new RttsRadioButton(this, 30, 65, 380, 24, "Add to level", valign_center);
    m_remove.Add(radio);

    radio = new RttsRadioButton(this, 30, 90, 380, 24, "Remove from level", valign_center);
    m_remove.Add(radio);

    m_remove.Set(0);

    // Start group
    radio = new RttsRadioButton(this, 30, 130, 380, 24, "at end of level", valign_center);
    m_start.Add(radio);

    radio = new RttsRadioButton(this, 30, 155, 380, 24, "at start of level", valign_center);
    m_start.Add(radio);

    m_start.Set(0);

    // Ok/Cancel buttons
    btn = new RttsButton(this, 30, 220, 0, 30, "OK", valign_center, 1);
    btn->Clicked.Connect(this, &ResizeDlg::Ok);
    SetDefaultButton(btn);

    btn = new RttsButton(this, 410, 220, 0, 30, "CANCEL", valign_center|halign_right, -1);
    btn->Clicked.Connect(this, &ResizeDlg::EndModal);
}

void ResizeDlg::Ok(int id)
{
    bool valid;

    valid = true;
    if (!m_pLines->IsValid())
        valid = false;

    if (!valid) {
        RttsMessageBox::Ok(this, "Please check all fields");
        return;
    }

    EndModal(1);
}

/*
==============================================================================

OptionsDlg IMPLEMENTATION

==============================================================================
*/

#define OPT_NONE        0
#define OPT_QUIT        1
#define OPT_TEST        2

class OptionsDlg : public RttsDialog {
public:
    Editor  *editor;

public:
    OptionsDlg(Editor *pParent);

    void ClearMap(int id);
    void Settings(int id);
    void LoadMap(int id);
    void SaveMap(int id);
    void ResizeMap(int id);
};

/*
==============
OptionsDlg::OptionsDlg

Builds the options dialog with all its elements
==============
*/
OptionsDlg::OptionsDlg(Editor *pParent)
    : RttsDialog(pParent, 150, 50, 300, 380)
{
    GButton *btn;

    editor = pParent;

    btn = new RttsButton(this, 30, 30, 260, 30, "Test map", valign_center|halign_center, OPT_TEST);
    btn->Clicked.Connect(this, &OptionsDlg::EndModal);

    btn = new RttsButton(this, 30, 60, 260, 30, "New", valign_center|halign_center, 0);
    btn->Clicked.Connect(this, &OptionsDlg::ClearMap);

    btn = new RttsButton(this, 30, 90, 260, 30, "Load", valign_center|halign_center, 0);
    btn->Clicked.Connect(this, &OptionsDlg::LoadMap);

    btn = new RttsButton(this, 30, 120, 260, 30, "Save", valign_center|halign_center, 0);
    btn->Clicked.Connect(this, &OptionsDlg::SaveMap);

    btn = new RttsButton(this, 30, 150, 260, 30, "Settings", valign_center|halign_center, 0);
    btn->Clicked.Connect(this, &OptionsDlg::Settings);

    btn = new RttsButton(this, 30, 180, 260, 30, "Resize", valign_center|halign_center, 0);
    btn->Clicked.Connect(this, &OptionsDlg::ResizeMap);

    btn = new RttsButton(this, 30, 210, 260, 30, "Quit", valign_center|halign_center, OPT_QUIT);
    btn->Clicked.Connect(this, &OptionsDlg::EndModal);

    btn = new RttsButton(this, 30, 240, 260, 30, "Return to editor",
            valign_center|halign_center, OPT_NONE);
    btn->Clicked.Connect(this, &OptionsDlg::EndModal);
}

/*
==============
OptionsDlg::ClearMap

Completely wipe the map
==============
*/
void OptionsDlg::ClearMap(int id)
{
    if (editor->m_bDirty) {
        if (!RttsMessageBox::YesNo(this, "This will destroy all changes. Continue?"))
            return;
    }

    editor->m_level.Clear();
    editor->m_level.Resize(true, 10);
    editor->m_level.SetDistance(0);
    editor->m_bDirty = false;
    EndModal(OPT_NONE);
}

/*
==============
OptionsDlg::Settings

Bring up the settings dialog
==============
*/
void OptionsDlg::Settings(int id)
{
    SettingsDlg dlg(this);
    int code;
    int baselevel;

    baselevel = editor->m_level.m_iBaseLevel;
    if (baselevel >= 0)
        dlg.PokeLevel(baselevel);
    dlg.PokeName(editor->m_level.m_szName);
    dlg.PokeAuthor(editor->m_level.m_szAuthor);

    code = dlg.Run();

    if (code > 0) {
        editor->m_level.m_iBaseLevel = dlg.PeekLevel();
        xstrcpy(editor->m_level.m_szName, sizeof(editor->m_level.m_szName),
                dlg.PeekName());
        xstrcpy(editor->m_level.m_szAuthor, sizeof(editor->m_level.m_szAuthor),
                dlg.PeekAuthor());
    }

    if (id < 0)
        EndModal(OPT_NONE);
}

/*
==============
OptionsDlg::SaveMap

Save the current map
==============
*/
void OptionsDlg::SaveMap(int id)
{
    bool valid;

    if (editor->m_level.m_iBaseLevel < 0 || !editor->m_level.m_szName[0] ||
        !editor->m_level.m_szAuthor[0])
        Settings(1);
    if (editor->m_level.m_iBaseLevel < 0 || !editor->m_level.m_szName[0] ||
        !editor->m_level.m_szAuthor[0])
        return;

    try {
        char buf[512];

        valid = editor->m_level.Save();
        editor->m_bDirty = false;

        snprintf(buf, sizeof(buf), "Saved map %04i - %s (%f screens)",
            editor->m_level.m_iBaseLevel+1, editor->m_level.m_szName,
            editor->m_level.m_iMapLength / 15.0);

        if (!valid)
            xstrcat(buf, sizeof(buf), "\nMap is not playable.");
        RttsMessageBox::Ok(this, buf);
    } catch(LError &err) {
        char buf[512];

        snprintf(buf, sizeof(buf), "Save failed: %s", err.Get());
        RttsMessageBox::Ok(this, buf);
    }

    EndModal(OPT_NONE);
}

/*
==============
OptionsDlg::LoadMap

Bring up the select map dialog and load the selected map
==============
*/
void OptionsDlg::LoadMap(int id)
{
    LoadDlg dlg(this);
    int code;

    if (editor->m_bDirty) {
        if (!RttsMessageBox::YesNo(this, "This will destroy all changes. Continue?"))
            return;
    }

    code = dlg.Run();

    if (code > 0) {
        try {
            char buf[256];
            bool playable;

            snprintf(buf, sizeof(buf), "levels/%s", dlg.PeekFilename());
            playable = editor->m_level.Load(buf);

            snprintf(buf, sizeof(buf), "Loaded map %04i - %s by %s",
                editor->m_level.m_iBaseLevel+1, editor->m_level.m_szName,
                editor->m_level.m_szAuthor);
            if (!playable)
                xstrcat(buf, sizeof(buf), ". Map is not playable.");
            RttsMessageBox::Ok(this, buf);
            editor->m_bDirty = false;
        } catch(LError &err) {
            char buf[256];
            snprintf(buf, sizeof(buf), "Load failed: %s", err.Get());
            RttsMessageBox::Ok(this, buf);
        }
    }

    EndModal(OPT_NONE);
}

/*
==============
OptionsDlg::ResizeMap

Prompts the user for # of lines
==============
*/
void OptionsDlg::ResizeMap(int id)
{
    ResizeDlg dlg(this);
    int code;
    bool start;
    int lines;

    code = dlg.Run();

    if (code > 0) {
        lines = dlg.PeekLines();
        if (dlg.PeekRemove())
            lines = -lines;
        start = dlg.PeekStart();

        editor->m_level.Resize(start, lines);
        editor->m_bDirty = true;
    }

    EndModal(OPT_NONE);
}


/*
==============================================================================

TestRunDlg IMPLEMENTATION

==============================================================================
*/

class TestRunDlg : public RttsDialog {
public:
    GCheckBox           *m_pWeapons;
    GCheckBox           *m_pFromCurpos;

    GRadioButtonGroup   m_level;

public:
    TestRunDlg(GPanel *pParent);

    void PeekPlayer(player_t *plr);
    int PeekLevel();
    inline bool PeekFromCurpos() { return m_pFromCurpos->m_bOn; }
};

TestRunDlg::TestRunDlg(GPanel *pParent)
    : RttsDialog(pParent, 100, 100, 440, 240)
{
    GButton *btn;
    GRadioButton *rb;

    m_pWeapons = new RttsCheckBox(this, 30, 30, 0, 0, "Additional weapons");

    rb = new RttsRadioButton(this, 30, 60, 0, 0, "Easy");
    m_level.Add(rb);
    rb = new RttsRadioButton(this, 30, 80, 0, 0, "Medium");
    m_level.Add(rb);
    rb = new RttsRadioButton(this, 30, 100, 0, 0, "Hard");
    m_level.Add(rb);
    m_level.Set(0);

    m_pFromCurpos = new RttsCheckBox(this, 30, 130, 0, 0, "Start from current pos");
    m_pFromCurpos->SetOn(true);

    btn = new RttsButton(this, 30, 200, 0, 0, "PLAY", halign_left, 1);
    btn->Clicked.Connect(this, &TestRunDlg::EndModal);

    SetDefaultButton(btn);

    btn = new RttsButton(this, 410, 200, 0, 0, "CANCEL", halign_right, -1);
    btn->Clicked.Connect(this, &TestRunDlg::EndModal);
}

void TestRunDlg::PeekPlayer(player_t *plr)
{
    int i;

    for(i = 0; i < NUM_WEAP; i++)
        plr->weapons[i] = m_pWeapons->m_bOn ? 1 : 0;
}

int TestRunDlg::PeekLevel()
{
    return m_level.Get();
}

/*
==============================================================================

Editor IMPLEMENTATION

==============================================================================
*/

/*
==============
MakeEditor

Avoids having to put the Editor declaration in a public header file
==============
*/
GPanel *MakeEditor()
{
    return new Editor;
}

/*
==============
Editor::Editor
==============
*/
Editor::Editor()
    : GPanel(0, 0, 0, 640, 480), m_level(&m_tilesets.m_tiletypes)
{
    GButton *pBtn;

    SetCanFocus(true);

    m_bDirty = false;

    m_level.SetEditMode(true);
    m_level.Resize(true, 10);
    m_level.SetDistance(0);

    m_iMode = -1;

    m_numunittypes = 0;
    m_unittypes = 0;

    LoadUnitTypes();

    m_tilesets.Load();

    m_flScrollSpeed = 0;
    m_iScrollTime = 0;

    pBtn = new RttsButton(this, 0, 60, 0, 25, "OPTIONS", 0);
    pBtn->Clicked.Connect(this, &Editor::Options);

    m_pModeButton = new RttsButton(this, 640, 60, 0, 25, "Mode: Tile", halign_right);
    m_pModeButton->Clicked.Connect(this, &Editor::ToggleMode);

    pBtn = new RttsButton(this, 640, 85, 0, 25, "Select", halign_right);
    pBtn->Clicked.Connect(this, &Editor::Mode_Pick);

    SetTLM(tlm_tile);
}

/*
==============
Editor::~Editor
==============
*/
Editor::~Editor()
{
    int i;

    LeaveTLM();

    for(i = 0; i < m_numunittypes; i++)
        Air_FreeType(m_unittypes[i]);
    L_Free(m_unittypes);
}

/*
==============
Editor::LoadUnitTypes

Load all the air/*.txt data
==============
*/
void Editor::LoadUnitTypes()
{
    char **names;
    char *p;
    int count;

    count = L_FindFiles("air", "*.txt", &names);
    m_unittypes = 0;
    m_numunittypes = 0;

    try
    {
        m_unittypes = (airunit_type_t **)L_Malloc(
                sizeof(airunit_type_t *)*count, TAG_EDITOR);

        for(m_numunittypes = 0; m_numunittypes < count; m_numunittypes++) {
            p = strstr(names[m_numunittypes], ".txt");
            if (p)
                *p = 0;
            m_unittypes[m_numunittypes] = Air_GetType(names[m_numunittypes]);
        }
    }
    catch(...)
    {
        if (m_unittypes) {
            while(m_numunittypes--)
                Air_FreeType(m_unittypes[m_numunittypes]);
            L_Free(m_unittypes);
        }
        L_FreeFindFiles(names);
        throw;
    }

    L_FreeFindFiles(names);
}

/*
==============
Editor::SetTLM

Change the top-level editor mode
==============
*/
void Editor::SetTLM(int mode)
{
    LeaveTLM();

    m_iMode = mode;

    // Initializes TLM-specific member variables
    switch(m_iMode) {
    case tlm_tile: Tile_Enter(); m_pModeButton->SetText("Mode: Tile"); break;
    case tlm_unit: Unit_Enter(); m_pModeButton->SetText("Mode: Units"); break;
    }
    m_pModeButton->AdjustSize();
}

/*
===============
Editor::LeaveTLM

Cleanup TLM-specific member variables
===============
*/
void Editor::LeaveTLM()
{
    switch(m_iMode) {
    case tlm_tile: Tile_Leave(); break;
    case tlm_unit: Unit_Leave(); break;
    }
}

/*
===============
Editor::SnapToGrid

Snap coordinates to a 16x16 grid
===============
*/
void Editor::SnapToGrid(float *px, float *py)
{
    float grid = 16.0;

    *px = grid * (int)(*px / grid);
    *py = grid * (int)(*py / grid);
}

/*
==============
Editor::EditorQuit

Check whether the map is dirty and ask for saving if needed.
==============
*/
void Editor::EditorQuit(int id)
{
    if (m_bDirty) {
        if (!RttsMessageBox::YesNo(this, "Quit the editor without saving?"))
            return;
    }

    EndModal(0);
}

/*
==============
Editor::Options

Bring up the Options dialog
==============
*/
void Editor::Options(int id)
{
    OptionsDlg opt(this);
    int code;

    // ouch!
    unit_Selected.clear();

    code = opt.Run();

    if (code == OPT_QUIT)
        EditorQuit(0);
    else if (code == OPT_TEST)
        TestRun();
}

/*
===============
Editor::Mode_Pick

Call the mode-specific "select item" function
===============
*/
void Editor::Mode_Pick(int id)
{
    switch(m_iMode) {
    case tlm_tile: Tile_Pick(); break;
    case tlm_unit: Unit_Pick(); break;
    }
}

/*
===============
Editor::ToggleMode

Cycle through editor modes
===============
*/
void Editor::ToggleMode(int id)
{
    switch(m_iMode) {
    default:
    case tlm_tile: SetTLM(tlm_unit); break;
    case tlm_unit: SetTLM(tlm_tile); break;
    }
}

/*
==============
Editor::TestRun

Load the level into the engine and get playing
==============
*/
void Editor::TestRun()
{
    EEngine engine;
    player_t plr;

    m_level.SetEditMode(false);
    engine.SetLevel(&m_level, true);

    memset(&plr, 0, sizeof(plr));
    xstrcpy(plr.name, sizeof(plr.name), "Editor");
    xstrcpy(plr.callsign, sizeof(plr.callsign), "1337");
    plr.level = 0;
    plr.health = 100;

    TestRunDlg dlg(this);
    int ret;

    ret = dlg.Run();

    if (ret > 0)
        {
        engine.SetLevelAdjust(-1, dlg.PeekLevel());

        dlg.PeekPlayer(&plr);
        engine.PokePlayer(&plr, config->GetItem("controls"));
        if (!dlg.PeekFromCurpos())
            m_level.SetDistance(0);

        engine.Run();
        }

    m_level.SetEditMode(true);
}


/*
==============
Editor::Scroll

Perform actual time-based scrolling with acceleration
==============
*/
void Editor::Scroll(float speed)
{
    float f;
    float dist;

    if (!speed) {
        m_flScrollSpeed = 0;
        m_iScrollTime = 0;
        return;
    }

    if (speed * m_flScrollSpeed < 0) { // "dot product" ;)
        m_flScrollSpeed = 0;
        m_iScrollTime = 0;
    }

    if (m_iScrollTime > 4000)
        m_iScrollTime = 4000;

    f = m_iScrollTime / 500.0;
    m_flScrollSpeed = speed * (1 + f);
    m_iScrollTime += hal->frametime;

    dist = m_level.m_flDistance + m_flScrollSpeed * hal->frametime / 1000.0;

    if (m_flScrollSpeed < 0) {
        if (m_level.m_flDistance <= 0)
            return;
        if (dist < 0)
            dist = 0;
    } else {
        if (m_level.m_flDistance >= m_level.m_iMapLength-15.0)
            return;
        if (dist > m_level.m_iMapLength-15)
            dist = m_level.m_iMapLength-15;
    }

    m_level.SetDistance(dist);
}

/*
==============
Editor::Logic

As long as the left mouse button is held down, the user can just
"paint" tiles
==============
*/
void Editor::Logic()
{
    byte *keystate;
    int btns, mx, my;
    float speed;

    btns = hal->GetMouseState(&mx, &my);
    keystate = hal->GetKeyState();

    switch(m_iMode) {
    case tlm_tile: Tile_Logic(keystate, btns, mx, my); break;
    }

    speed = 0;
    if (my < 40)
        speed += 5;
    else if (my > 440)
        speed -= 5;

    if (keystate[KEY_UP])
        speed += 5;
    if (keystate[KEY_DOWN])
        speed -= 5;

    Scroll(speed);
}

/*
==============
Editor::DrawAirJobs

Draw all airjobs including selection boxes etc...
==============
*/
void Editor::DrawAirJobs()
{
    airjob_it aj;
    float x, y;
    float sx, sy;
    int a;

    if (m_iMode == tlm_unit)
        a = 255;
    else
        a = 128;

    // draw shadows
    hal->SetAlphaOnly(true);

    for(aj = m_level.m_AirJobs.begin(); aj != m_level.m_AirJobs.end(); aj++) {
        spr_frame_t *frame = &aj->type->sprite->base.frames[0];

        m_level.LevelToScreen(aj->x, aj->y, &x, &y);
        sx = SHADOWX(x);
        sy = SHADOWY(y);

        sx += frame->offsets[0];
        sy += frame->offsets[1];

        frame->pic->Draw(sx, sy, 255, 255, 255, (a*64)/256);
    }

    hal->SetAlphaOnly(false);

    // draw units
    for(aj = m_level.m_AirJobs.begin(); aj != m_level.m_AirJobs.end(); aj++)
        Unit_DrawAirjob(&*aj, 0, 0, a);

    // draw movement nodes
    if (m_iMode == tlm_unit)
    {
        for(airjob_it aj = m_level.m_AirJobs.begin(); aj != m_level.m_AirJobs.end(); aj++)
            Unit_DrawAirjobNodes(&*aj, 0, 0, a);
    }
}

/*
==============
Editor::Draw

Get the map to draw
==============
*/
void Editor::Draw()
{
    m_level.Draw();

//  if (m_iMode == tlm_unit)
        DrawAirJobs();

    switch(m_iMode) {
    case tlm_tile: Tile_Draw(); break;
    case tlm_unit: Unit_Draw(); break;
    }
}

/*
==============
Editor::Key

Escape quits the editor
==============
*/
bool Editor::Key(int code, char c, bool down)
{
    switch(m_iMode) {
    case tlm_tile:
        if (Tile_Key(code, c, down))
            return true;
        break;

    case tlm_unit:
        if (Unit_Key(code, c, down))
            return true;
        break;
    }

    if (down) {
        switch(code) {
        case KEY_ESCAPE: Options(-1); return true;
        case KEY_F5: TestRun(); return true;
        case KEY_F6: ToggleMode(-1); return true;

        case KEY_t: SetTLM(tlm_tile); Tile_Pick(); return true;
        case KEY_u: SetTLM(tlm_unit); Unit_Pick(); return true;

        case KEY_UP:
        case KEY_DOWN:
            return true; // blocked for scrolling (see Logic)
        }
    }

    return GPanel::Key(code, c, down);
}

/*
==============
Editor::MouseButton

Multiplex mouse-clicks to the handler for the current mode
==============
*/
bool Editor::MouseButton(int btn, int x, int y, bool down)
{
    // mode specific actions override
    switch(m_iMode) {
    case tlm_tile:
        if (Tile_MouseButton(btn, x, y, down))
            return true;
        break;
    case tlm_unit:
        if (Unit_MouseButton(btn, x, y, down))
            return true;
        break;
    }

    return GPanel::MouseButton(btn, x, y, down);
}

/*
==============
Editor::MouseMove

Handle mouse moves by the appropriate handler
==============
*/
void Editor::MouseMove(int x, int y)
{
    switch(m_iMode) {
    //case tlm_tile: Tile_MouseMove(x, y); break;
    case tlm_unit: Unit_MouseMove(x, y); break;
    }
}

/*
==============================================================================

tlm_tile -- Tile editing mode

==============================================================================
*/

/*
===============
Editor::Tile_Enter

Begin with tile editing (called from SetTLM()).
===============
*/
void Editor::Tile_Enter()
{
    m_bInPaint = false;
}

/*
===============
Editor::Tile_Leave

End tile editing
===============
*/
void Editor::Tile_Leave()
{
}

/*
===============
Editor::Tile_FixTile

Fix the tile so that the borders match the surroundings.
change is a bitmask with the borders that can be violated if necessary.
A bitmask of borders that have actually been changed is returned.
===============
*/
int Editor::Tile_FixTile(int tx, int ty, int change)
{
    ed_tiletype_t *ett, *curett;
    unsigned id;
    LPool<unsigned> tp_perfect;
    LPool<unsigned> tp_change;
    int borders[4]; // desired borders (-1 = any)
    int x, y, i;

    id = m_level.GetTile(tx, ty);
    if (!m_tilesets.m_tiletypes.ValidId(id))
        return 0; // *never* replace an unset tile, gets messy as hell
    curett = m_tilesets.GetTileTypeEx(id);

    // determine the perfect borders
    for(i = 0; i < 4; i++) {
        // get neighbouring x/y
        switch(i) {
        case 0: x = tx-1; y = ty; break;
        case 1: x = tx+1; y = ty; break;
        case 2: x = tx; y = ty-1; break;
        case 3: x = tx; y = ty+1; break;
        }

        if (x < 0 || x >= MAPWIDTH || y < 0 || y >= m_level.m_iMapLength) {
            borders[i] = curett->border[i];
            continue;
        }

        // get the neighbour's border attributes
        id = m_level.GetTile(x, y);
        if (!m_tilesets.m_tiletypes.ValidId(id)) {
            // keep the current border, if possible
            borders[i] = curett->border[i];
            continue;
        }

        ett = m_tilesets.GetTileTypeEx(id);

        borders[i] = ett->border[i ^ 1];
    }

    // do we need to change anything?
    for(i = 0; i < 4; i++) {
        if (borders[i] < 0 || curett->border[i] < 0)
            continue;
        if (borders[i] != curett->border[i])
            break;
    }
    if (i == 4)
        return 0; // nothing of significance changed

    // walk all tiles to find suitable ones
    for(id = 0; id < m_tilesets.m_tiletypes.m_iNumTT; id++) {
        bool changes;

        ett = m_tilesets.GetTileTypeEx(id);
        if (ett->noauto)
            continue;

        // check the borders
        changes = false;
        for(i = 0; i < 4; i++) {
            if (ett->border[i] < 0)
                goto skiptile; // only use well-defined tiles
            if (borders[i] >= 0 && ett->border[i] != borders[i]) {
                changes = true;
                if (!((1 << i) & change))
                    goto skiptile;
            }
        }

        if (changes)
            tp_change.Add(id);
        else
            tp_perfect.Add(id);

skiptile: ;
    }

    // try to find a replacement tile
    if (tp_perfect.size) {
        i = rand() % tp_perfect.size;
        id = tp_perfect.items[i];
        change = 0;
    } else if (tp_change.size) {
        i = rand() % tp_change.size;
        id = tp_change.items[i];
        ett = m_tilesets.GetTileTypeEx(id);

        change = 0;
        for(i = 0; i < 4; i++) {
            if (borders[i] >= 0 && borders[i] != ett->border[i])
                change |= (1 << i);
        }
    } else
        return 0;

    m_level.SetTile(tx, ty, id);

    return change;
}

/*
===============
Editor::Tile_DoPlace

Place the tile with the given ID at the given coordinates.
Automatically adjust neighbouring tiles if necessary
===============
*/
void Editor::Tile_DoPlace(int tx, int ty, int tile)
{
    unsigned oldtile;
    int modmask, mask, change;
    int x, y, i;

    lassert(m_tilesets.m_tiletypes.ValidId(tile));

    // Save original tile and place new one
    oldtile = m_level.GetTile(tx, ty);

    m_level.SetTile(tx, ty, tile);

    // Fix the direct neighbour if a border has changed
    modmask = 0;
    for(i = 0; i < 4; i++) {
        ed_tiletype_t *orig, *cur;

        if (m_tilesets.m_tiletypes.ValidId(oldtile)) {
            orig = m_tilesets.GetTileTypeEx(oldtile);
            cur = m_tilesets.GetTileTypeEx(tile);
            if (orig->border[i] == cur->border[i])
                continue;
        }

        switch(i) {
        case 0: x = tx-1; y = ty; change = 12; mask = 0; break;
        case 1: x = tx+1; y = ty; change = 12; mask = 1; break;
        case 2: x = tx; y = ty-1; change = 3; mask = 0; break;
        case 3: x = tx; y = ty+1; change = 3; mask = 2; break;
        }

        if (x < 0 || x >= MAPWIDTH || y < 0 || y >= m_level.m_iMapLength)
            continue;

        change = Tile_FixTile(x, y, change);

        if (change & 1)
            modmask |= 1 << mask;
        if (change & 2)
            modmask |= 1 << (mask | 1);
        if (change & 4)
            modmask |= 1 << mask;
        if (change & 8)
            modmask |= 1 << (mask | 2);
    }

    // Fix the indirect neighbours that need fixing
    for(i = 0; i < 4; i++) {
        if (!(modmask & (1 << i)))
            continue;

        x = tx + ((i & 1) ? 1 : -1);
        y = ty + ((i & 2) ? 1 : -1);

        Tile_FixTile(x, y, 0);
    }

    m_bDirty = true;
}

/*
==============
Editor::Tile_Logic

Tile painting
==============
*/
void Editor::Tile_Logic(byte *keystate, int btns, int mx, int my)
{
    if (m_bInPaint) {
        m_bInPaint = false;
        if (IsMouseOver() && (btns & 1) && m_tilesets.isCurTileValid()) {
            int tx, ty;
            unsigned id;

            m_level.TileAtPoint(mx, my, &tx, &ty);
            id = m_level.GetTile(tx, ty);
            if (id != m_tilesets.m_iCurTile)
                Tile_DoPlace(tx, ty, m_tilesets.m_iCurTile);
            m_bInPaint = true;
        }
    }
}

/*
==============
Editor::Tile_MouseButton

Toggle paint mode. This locks painting out when one of the buttons is
pressed.
The actual painting is done in Tile_Logic().

Also handles pipette (right-click) action.
==============
*/
bool Editor::Tile_MouseButton(int btn, int x, int y, bool down)
{
    if (btn == 0)
    {
        if (down)
            m_bInPaint = true;
        else
            m_bInPaint = false;

        return true;
    }
    else if (btn == 1)
    {
        if (down)
        {
            int tx, ty;
            unsigned id;

            m_level.TileAtPoint(x, y, &tx, &ty);
            id = m_level.GetTile(tx, ty);
            if (m_tilesets.m_tiletypes.ValidId(id))
                m_tilesets.SetCurTile(id);
        }
        return true;
    }

    return false;
}

/*
==============
Editor::Tile_Key

Cycle through tile types
==============
*/
bool Editor::Tile_Key(int code, char c, bool down)
{
    if (down) {
        switch(code) {
        case KEY_LEFT: m_tilesets.PrevTile(); break;
        case KEY_RIGHT: m_tilesets.NextTile(); break;
        }
    }

    return false;
}

/*
==============
Editor::Tile_Draw

Draw the hovering tile preview (if a tile has been selected).
==============
*/
void Editor::Tile_Draw()
{
    int mx, my;
    int tx, ty;
    float x, y;

    if (!m_tilesets.isCurTileValid())
        return;

    hal->GetMouseState(&mx, &my);
    m_level.TileAtPoint(mx, my, &tx, &ty);
    m_level.PointForTile(tx, ty, &x, &y);

    m_tilesets.m_tiletypes.m_pTT[m_tilesets.m_iCurTile].pic->Draw(x, y, 255, 255, 255, 192);
}


/*
==============================================================================

tlm_unit - Unit editing mode

==============================================================================
*/

/*
===============
Editor::SetCurUnit
Editor::SetCurUnitType

Changes which unit is currently selected for placing
===============
*/
void Editor::SetCurUnit(int id)
{
    lassert(id >= 0 && id < m_numunittypes);

    SetCurUnitByType(m_unittypes[id]);
}

void Editor::SetCurUnitByType(airunit_type_t *type)
{
    airjob_t aj(type);
    unit_Formation.clear();
    unit_Formation.push_back(aj);
}

/*
===============
Editor::Unit_Enter

Enter unit placement mode (called by SetTLM()).
===============
*/
void Editor::Unit_Enter()
{
    unit_Formation.clear();

    unit_bSelecting = false;
    unit_Selected.clear();

    unit_bDragStart = false;
}

/*
===============
Editor::Unit_Leave

End unit placement mode
===============
*/
void Editor::Unit_Leave()
{
}

/*
===============
Editor::Unit_IsSelected

Returns true if the given airjob is in the selection list
===============
*/
bool Editor::Unit_IsSelected(airjob_t *aj)
{
    return std::find(unit_Selected.begin(), unit_Selected.end(), aj) != unit_Selected.end();
}

/*
===============
Editor::Unit_AddToSelection

Add the airjobs listed in the given vector to the current selection list.
===============
*/
void Editor::Unit_AddToSelection(const std::vector<airjob_t*> &list)
{
    for(airjobp_cit aj = list.begin(); aj != list.end(); aj++) {
        if (!Unit_IsSelected(*aj))
            unit_Selected.push_back(*aj);
    }
}

/*
===============
Editor::Unit_RemoveFromSelection

Remove the airjobs listed in the given vector from the current selection list
===============
*/
void Editor::Unit_RemoveFromSelection(const std::vector<airjob_t*> &list)
{
    for(airjobp_cit aj = list.begin(); aj != list.end(); aj++) {
        airjobp_it place = std::find(unit_Selected.begin(), unit_Selected.end(), *aj);
        if (place != unit_Selected.end())
            unit_Selected.erase(place);
    }
}

/*
===============
Editor::Unit_DragStart

Begin dragging. After you've called this function, you must make sure to call
the other Unit_Drag* functions until you're done.
===============
*/
void Editor::Unit_DragStart(float lx, float ly, bool selectiffail)
{
    unit_bDragStart = true;
    unit_bDrag = false;
    unit_bDragSelectIfFail = selectiffail;

    unit_flDragX = lx;
    unit_flDragY = ly;

    unit_pDragNodeJob = 0;

    GrabMouse(true);
}

/*
===============
Editor::Unit_DragStart

Begin dragging a node.
===============
*/
void Editor::Unit_DragStart(float lx, float ly, airjob_t *aj, int idx)
{
    unit_bDragStart = true;
    unit_bDrag = false;
    unit_bDragSelectIfFail = false;

    unit_flDragX = lx;
    unit_flDragY = ly;

    unit_pDragNodeJob = aj;
    unit_iDragNodeIdx = idx;

    GrabMouse(true);
}

/*
===============
Editor::Unit_DragUpdate

Call when the mouse pos has changed
===============
*/
void Editor::Unit_DragUpdate(float lx, float ly)
{
    float sx, sy;
    float dx, dy;

    sx = unit_flDragX;
    sy = unit_flDragY;
    SnapToGrid(&sx, &sy);

    SnapToGrid(&lx, &ly);

    dx = lx - sx;
    dy = ly - sy;

    if (dx || dy) {
        if (!unit_pDragNodeJob)
        {
            for(airjobp_it aj = unit_Selected.begin(); aj != unit_Selected.end(); aj++)
                m_level.SetAirjobPos(*aj, (*aj)->x + dx, (*aj)->y + dy);
        }
        else
        {
            byte *keystate = hal->GetKeyState();;
            bool shift = keystate[KEY_LSHIFT] || keystate[KEY_RSHIFT];
            jobnode_t *jn;

            jn = &unit_pDragNodeJob->nodes[unit_iDragNodeIdx];
            jn->dx += dx;
            jn->dy -= dy;

            SnapToGrid(&jn->dx, &jn->dy);

            if (!shift) {
                if (unit_iDragNodeIdx+1 < unit_pDragNodeJob->nodes.size()) {
                    jn++;
                    jn->dx -= dx;
                    jn->dy += dy;

                    SnapToGrid(&jn->dx, &jn->dy);
                }
            }
        }

        unit_flDragX += dx;
        unit_flDragY += dy;
        unit_bDrag = true;

        m_bDirty = true;
    }
}

/*
===============
Editor::Unit_DragEnd

Called when we stop dragging
===============
*/
void Editor::Unit_DragEnd()
{
    unit_bDragStart = false;
    GrabMouse(false);

    // cull merged nodes
    if (unit_pDragNodeJob) {
        airjob_t *aj = unit_pDragNodeJob;
        int i = 0;
        while(i < aj->nodes.size()) {
            jobnode_it jn = aj->nodes.begin() + i;
            if (fabs(jn->dx) < 0.1 && fabs(jn->dy) < 0.1)
                aj->nodes.erase(jn);
            else
                i++;
        }
    }

    // if we didn't actually drag anything it means that we should select
    // the unit the user original clicked on
    if (!unit_bDrag && unit_bDragSelectIfFail) {
        airjob_t *aj = m_level.AirJobAtPoint(unit_flDragX, unit_flDragY);

        if (aj) {
            std::vector<airjob_t*> units;
            units.push_back(aj);

            unit_Selected = units;
        }
    }
}

/*
===============
Editor::Unit_Action_Level

Cycle the level of selected units
===============
*/
void Editor::Unit_Action_Level()
{
    if (unit_Selected.empty())
        return;

    for(airjobp_it job = unit_Selected.begin(); job != unit_Selected.end(); job++) {
        (*job)->level++;
        if ((*job)->level > 2)
            (*job)->level = 0;
    }

    m_bDirty = true;
}

/*
===============
Editor::Unit_Action_Mirror

Apply a vertical mirror to the currently selected units and their nodes
===============
*/
void Editor::Unit_Action_Mirror()
{
    airjobp_it aj;
    float x;

    if (unit_Selected.empty())
        return;

    // determine the average X-position
    x = 0;
    for(aj = unit_Selected.begin(); aj != unit_Selected.end(); aj++)
        x += (*aj)->x;
    x /= unit_Selected.size();

    // actually perform the mirroring
    for(aj = unit_Selected.begin(); aj != unit_Selected.end(); aj++) {
        m_level.SetAirjobPos(*aj, x + (x - (*aj)->x), (*aj)->y);

        for(jobnode_it node = (*aj)->nodes.begin(); node != (*aj)->nodes.end(); node++)
            node->dx = -node->dx;
    }

    m_bDirty = true;
}

/*
===============
Editor::Unit_Action_Copy

Copy the currently selected units into the formation so that they can now
be placed.
===============
*/
void Editor::Unit_Action_Copy()
{
    if (unit_Selected.empty())
        return;

    unit_Formation.clear();

    airjobp_it aj = unit_Selected.begin();
    float x = (*aj)->x;
    float y = (*aj)->y;
    do {
        airjob_t copy(**aj);
        copy.x -= x;
        copy.y -= y;
        unit_Formation.push_back(copy);

        aj++;
    } while(aj != unit_Selected.end());

    unit_Selected.clear();
}

/*
===============
Editor::Unit_Action_Delete

Delete the currently selected units
===============
*/
void Editor::Unit_Action_Delete()
{
    if (unit_Selected.empty())
        return;

    m_level.DelAirjobs(unit_Selected);
    m_bDirty = true;

    unit_Selected.clear();
}

/*
===============
Editor::Unit_Node_Add

Add a new node after the current one
===============
*/
void Editor::Unit_Node_Add()
{
    airjob_t *aj = unit_pEditNodeJob;

    if (!aj)
        return;

    if (unit_iEditNodeIdx+1 >= aj->nodes.size()) {
        jobnode_t jn;
        jn.dx = 0;
        jn.dy = 128;
        aj->nodes.push_back(jn);
    } else {
        jobnode_t *next;
        jobnode_t jn;

        next = &aj->nodes[unit_iEditNodeIdx+1];
        jn.dx = next->dx/2;
        jn.dy = next->dy/2;
        next->dx -= jn.dx;
        next->dy -= jn.dy;

        aj->nodes.insert(aj->nodes.begin() + unit_iEditNodeIdx+1, jn);
    }

    m_bDirty = true;
}

/*
===============
Editor::Unit_Node_AddRel

Add a new relative node
===============
*/
void Editor::Unit_Node_AddRel()
{
    airjob_t *aj = unit_pEditNodeJob;

    if (!aj)
        return;

    jobnode_t jn;
    jn.dx = 0;
    jn.dy = 128;
    aj->nodes.insert(aj->nodes.begin() + unit_iEditNodeIdx+1, jn);

    m_bDirty = true;
}

/*
===============
Editor::Unit_Node_Delete

Delete the given node; ensure that the next node doesn't change position
===============
*/
void Editor::Unit_Node_Delete()
{
    airjob_t *aj = unit_pEditNodeJob;

    if (!aj)
        return;

    if (unit_iEditNodeIdx+1 == aj->nodes.size()) {
        aj->nodes.pop_back();
    } else {
        jobnode_t *jn = &aj->nodes[unit_iEditNodeIdx];
        jobnode_t *next = jn+1;

        next->dx += jn->dx;
        next->dy += jn->dy;

        aj->nodes.erase(aj->nodes.begin() + unit_iEditNodeIdx);
    }

    m_bDirty = true;
}

/*
===============
Editor::Unit_Node_DeleteRel

Delete a node relatively, i.e. subsequent nodes will change position
===============
*/
void Editor::Unit_Node_DeleteRel()
{
    airjob_t *aj = unit_pEditNodeJob;

    if (!aj)
        return;

    aj->nodes.erase(aj->nodes.begin() + unit_iEditNodeIdx);
    m_bDirty = true;
}

/*
===============
Editor::Unit_RightClick

Bring up the right-click action menu for the currently selected unit(s)
===============
*/
void Editor::Unit_RightClick()
{
    GPopupMenu menu;

    menu.AddItem("Level")->signaled.Connect(this, &Editor::Unit_Action_Level);
    menu.AddItem("Mirror")->signaled.Connect(this, &Editor::Unit_Action_Mirror);
    menu.AddItem("Copy")->signaled.Connect(this, &Editor::Unit_Action_Copy);
//  menu.AddItem("Delete")->signaled.Connect(this, &Editor::Unit_Action_Delete);

    menu.DisplayNearMouse();
}

/*
===============
Editor::Unit_NodeRightClick

Bring up the right-click action menu for the clicked-on node
===============
*/
void Editor::Unit_NodeRightClick(airjob_t *aj, int idx)
{
    GPopupMenu menu;

    unit_pEditNodeJob = aj;
    unit_iEditNodeIdx = idx;

    menu.AddItem("Add Node")->signaled.Connect(this, &Editor::Unit_Node_Add);
    menu.AddItem("Add Node (rel)")->signaled.Connect(this, &Editor::Unit_Node_AddRel);

    if (idx >= 0) {
        menu.AddItem("Delete Node")->signaled.Connect(this, &Editor::Unit_Node_Delete);
        menu.AddItem("Delete Node (rel)")->signaled.Connect(this, &Editor::Unit_Node_DeleteRel);
    } else {
        menu.AddItem("Level")->signaled.Connect(this, &Editor::Unit_Action_Level);
        menu.AddItem("Mirror")->signaled.Connect(this, &Editor::Unit_Action_Mirror);
        menu.AddItem("Copy")->signaled.Connect(this, &Editor::Unit_Action_Copy);
//      menu.AddItem("Delete")->signaled.Connect(this, &Editor::Unit_Action_Delete);
    }

    menu.DisplayNearMouse();

    unit_pEditNodeJob = 0;
}

/*
===============
Editor::Unit_MouseButton

Place units on left-click, handle selections, etc...
===============
*/
bool Editor::Unit_MouseButton(int btn, int x, int y, bool down)
{
    byte *keystate = hal->GetKeyState();;
    bool shift = keystate[KEY_LSHIFT] || keystate[KEY_RSHIFT];
    bool ctrl = keystate[KEY_LCTRL] || keystate[KEY_RCTRL];
    float lx, ly;

    m_level.ScreenToLevel(x, y, &lx, &ly);

    // LEFT CLICK: Selection, dragging
    if (btn == 0) {
        // Left mouse-button pressed
        if (down)
        {
            airjob_t *aj;
            int idx;

            // If there's a (non-root) node here, start dragging it
            if (unit_Selected.empty())
                aj = m_level.NodeAtPoint(lx, ly, &idx);
            else
                aj = m_level.NodeAtPoint(unit_Selected, lx, ly, &idx);

            if (aj && idx >= 0) {
                unit_Formation.clear();

                Unit_DragStart(lx, ly, aj, idx);

                return true;
            }

            // If there's a unit here, select it and possibly start dragging
            aj = m_level.AirJobAtPoint(lx, ly);
            if (aj) {
                std::vector<airjob_t*> units;
                units.push_back(aj);

                unit_Formation.clear(); // no longer place units

                if (ctrl)
                    Unit_RemoveFromSelection(units);
                else {
                    bool selectiffail;

                    if (shift) {
                        Unit_AddToSelection(units);
                        selectiffail = false;
                    } else {
                        if (!Unit_IsSelected(aj))
                            unit_Selected = units;
                        else
                            selectiffail = true;
                    }

                    Unit_DragStart(lx, ly, selectiffail);
                }

                return true;
            }

            // Place a unit
            if (!unit_Formation.empty()) {
                SnapToGrid(&lx, &ly);
                m_level.AddAirjobs(unit_Formation, lx, ly);
                unit_Selected.clear();
                m_bDirty = true;

                return true;
            }

            // If no unit type is applied for placing, start selecting
            GrabMouse(true);

            unit_Formation.clear();

            unit_bSelecting = true;
            unit_flSelectStartX = lx;
            unit_flSelectStartY = ly;

            return true;
        }
        else
        {
            // Complete dragging action
            if (unit_bDragStart) {
                Unit_DragEnd();
                return true;
            }

            // Complete selecting action
            if (unit_bSelecting) {
                std::vector<airjob_t*> units;

                m_level.FindAirJobs(unit_flSelectStartX, unit_flSelectStartY, lx, ly, &units);

                if (ctrl)
                    Unit_RemoveFromSelection(units);
                else if (shift)
                    Unit_AddToSelection(units);
                else
                    unit_Selected = units;

                unit_bSelecting = false;
                GrabMouse(false);

                return true;
            }

            return true;
        }
    }

    // RIGHT CLICK: Context menu
    if (btn == 1) {
        if (down)
        {
            airjob_t *aj;
            int idx;

            // We assume that the user wants to edit an existing unit when right-clicking
            // Therefore, the formation is cleared
            unit_Formation.clear();

            // Edit nodes
            if (unit_Selected.empty())
                aj = m_level.NodeAtPoint(lx, ly, &idx);
            else
                aj = m_level.NodeAtPoint(unit_Selected, lx, ly, &idx);

            if (aj) {
                Unit_NodeRightClick(aj, idx);
                return true;
            }

            // If there's a unit here, bring up the context menu.
            // If the unit isn't currently selected, make it the only selection.
            aj = m_level.AirJobAtPoint(lx, ly);
            if (aj) {
                if (!Unit_IsSelected(aj)) {
                    unit_Selected.clear();
                    unit_Selected.push_back(aj);
                }

                Unit_RightClick();
                return true;
            }
        }
        else
        {
            // does nothing ATM
            return true;
        }
    }

    return false;
}

/*
===============
Editor::Unit_MouseMove

Update while dragging
===============
*/
void Editor::Unit_MouseMove(int x, int y)
{
    float lx, ly;

    m_level.ScreenToLevel(x, y, &lx, &ly);

    if (unit_bDragStart) {
        Unit_DragUpdate(lx, ly);
        return;
    }
}

/*
===============
Editor::Unit_Key

Process unit editing shortcuts
===============
*/
bool Editor::Unit_Key(int code, char c, bool down)
{
    byte *keystate = hal->GetKeyState();;
    bool shift = keystate[KEY_LSHIFT] || keystate[KEY_RSHIFT];
    bool ctrl = keystate[KEY_LCTRL] || keystate[KEY_RCTRL];

    switch(code) {
    case KEY_DELETE:
        if (down)
            Unit_Action_Delete();
        return true;

    case KEY_l:
        if (down)
            Unit_Action_Level();
        return true;

    case KEY_h:
        if (down)
            Unit_Action_Mirror();
        return true;

    case KEY_c:
        if (ctrl) {
            if (down)
                Unit_Action_Copy();
            return true;
        }
        break;
    }

    return false;
}

/*
===============
Editor::Unit_Draw

Draw the hovering unit preview
===============
*/
void Editor::Unit_Draw()
{
    int mx, my;
    float sx, sy;

    hal->GetMouseState(&mx, &my);

    // print the unit preview
    if (unit_Formation.size())
    {
        airjob_it job;

        m_level.LevelToScreen(0, 0, &sx, &sy);

        for(job = unit_Formation.begin(); job != unit_Formation.end(); job++)
            Unit_DrawAirjob(&*job, mx - sx, my - sy, 160);
        for(job = unit_Formation.begin(); job != unit_Formation.end(); job++)
            Unit_DrawAirjobNodes(&*job, mx - sx, my - sy, 160);
    }

    // print the selection dragging
    if (unit_bSelecting)
    {
        m_level.LevelToScreen(unit_flSelectStartX, unit_flSelectStartY, &sx, &sy);

        hal->DrawLine(mx, my, sx, my, 255, 255, 255);
        hal->DrawLine(sx, my, sx, sy, 255, 255, 255);
        hal->DrawLine(sx, sy, mx, sy, 255, 255, 255);
        hal->DrawLine(mx, sy, mx, my, 255, 255, 255);
    }
}

/*
===============
Editor::Unit_DrawAirjob

Draw the airjob, and if applicable additional stuff like selections

NOTE: This function can be called while not in tlm_unit mode; however, it may
choose to access unit_* data when in this mode.
===============
*/
void Editor::Unit_DrawAirjob(airjob_t *aj, float relx, float rely, int a)
{
    float x, y;
    sprite_t *spr;

    m_level.LevelToScreen(aj->x, aj->y, &x, &y);
    x += relx;
    y += rely;

    spr = aj->type->sprite;
    SPR_Draw(spr, x, y, -1, hal->framestart, a);

    if (m_iMode == tlm_unit)
    {
        // draw selection markers if necessary
        if (Unit_IsSelected(aj)) {
            float x1, y1, x2, y2;

            x1 = x + spr->mins[0] - 3;
            y1 = y + spr->mins[1] - 3;
            x2 = x + spr->maxs[0] + 3;
            y2 = y + spr->maxs[1] + 3;

            hal->DrawLine(x1, y1, x1+10, y1, 255, 192, 0);
            hal->DrawLine(x1, y1, x1, y1+10, 255, 192, 0);

            hal->DrawLine(x2, y1, x2-10, y1, 255, 192, 0);
            hal->DrawLine(x2, y1, x2, y1+10, 255, 192, 0);

            hal->DrawLine(x2, y2, x2-10, y2, 255, 192, 0);
            hal->DrawLine(x2, y2, x2, y2-10, 255, 192, 0);

            hal->DrawLine(x1, y2, x1+10, y2, 255, 192, 0);
            hal->DrawLine(x1, y2, x1, y2-10, 255, 192, 0);
        }
    }
}

/*
===============
Editor::Unit_DrawAirjobNodes

Draw the movement nodes belonging to an airjob

NOTE: This function can be called while not in tlm_unit mode; however, it may
choose to access unit_* data when in this mode.
===============
*/
void Editor::Unit_DrawAirjobNodes(airjob_t *aj, float relx, float rely, int a)
{
    int idx;
    int r, g, b;
    float x, y;
    float x2, y2;

    if (!unit_Selected.empty())
    {
        if (std::find(unit_Selected.begin(), unit_Selected.end(), aj) == unit_Selected.end())
            a = (a*112)/255;
    }

    m_level.LevelToScreen(aj->x, aj->y, &x, &y);
    x += relx;
    y += rely;

    if (aj->level == 0) {
        r = 0;
        g = 255;
        b = 0;
    } else if (aj->level == 1) {
        r = 255;
        g = 128;
        b = 0;
    } else {
        r = 255;
        g = 0;
        b = 0;
    }

    idx = 0;
    for(;;) {
        hal->FillRect((int)(x-4), (int)(y-4), 8, 8, r, g, b, a);

        if (idx >= aj->nodes.size())
            break;

        x2 = x + aj->nodes[idx].dx;
        y2 = y + aj->nodes[idx].dy;

        hal->DrawLine(x, y, x2, y2, r, g, b, a);

        x = x2;
        y = y2;
        idx++;
    }
}

/*
==============
Editor::Unit_Pick

Open the pick unit dialog
==============
*/
void Editor::Unit_Pick()
{
    lassert(m_iMode == tlm_unit);

    int code;
    PickUnitDlg dlg(this, m_numunittypes, m_unittypes);

    code = dlg.Run();

    if (code < 0)
        return;

    unit_Selected.clear();
    SetCurUnit(code);
}

#if 0
//============================================================================
// em_sel_unit mode


/*
==============
Editor::Select_MouseButton

Simple left-click changes selection to what the mouse is over.
SHIFT+left-click toggles selection of a single unit
==============
*/
bool Editor::Select_MouseButton(int btn, int x, int y, bool down)
{
    airjob_t *aj;
    byte *keystate;
    float lx, ly;
    bool shift;

    if (btn == 0)
    {
        keystate = hal->GetKeyState();
        m_level.ScreenToLevel(x, y, &lx, &ly);

        shift = keystate[KEY_LSHIFT] || keystate[KEY_RSHIFT];

        aj = m_level.AirJobAtPoint(lx, ly);

        if (down) {
            if (aj && IsSelected(aj)) {
                m_flSelectDragX = lx;
                m_flSelectDragY = ly;
                SnapToGrid(&m_flSelectDragX, &m_flSelectDragY);

                m_bSelectDragStart = true;
            }
        } else {
            if (!m_bSelectDrag) {
                if (shift) {
                    if (aj)
                        ToggleSelect(aj);
                } else {
                    L_Free(m_pSelected);
                    m_pSelected = 0;
                    m_iNumSelected = 0;

                    if (aj)
                        ToggleSelect(aj);
                    else
                        SetMode(em_none);
                }
            }

            m_bSelectDragStart = false;
            m_bSelectDrag = false;
        }

        return true;
    }

    return false;
}

/*
==============
Editor::Select_MouseMove

If dragging, move all selected units
==============
*/
void Editor::Select_MouseMove(int x, int y)
{
    float lx, ly;
    float dx, dy;
    airjob_t *aj;
    int i;

    if (m_bSelectDragStart) {
        m_level.ScreenToLevel(x, y, &lx, &ly);
        SnapToGrid(&lx, &ly);

        dx = lx - m_flSelectDragX;
        dy = ly - m_flSelectDragY;

        if (dx || dy) {
            for(i = 0; i < m_iNumSelected; i++) {
                aj = m_pSelected[i];

                m_level.SetAirjobPos(aj, aj->x + dx, aj->y + dy);
            }

            m_flSelectDragX = lx;
            m_flSelectDragY = ly;
            m_bSelectDrag = true;

            m_bDirty = true;
        }
    }
}

/*
==============
Editor::Select_Key

DEL kills all airjobs in the current selection
==============
*/
bool Editor::Select_Key(int code, char c, bool down)
{
    int i;

    if (down) {
        if (code == KEY_DELETE) {
            if (m_bNodeDrag) // danger of deleting active airjob
                return false;

            for(i = 0; i < m_iNumSelected; i++)
                m_level.DelAirJob(m_pSelected[i]);
            ClearSelect();
            m_bDirty = true;
            return true;
        } else if (code == KEY_l) {
            for(i = 0; i < m_iNumSelected; i++) {
                m_pSelected[i]->level++;
                if (m_pSelected[i]->level > 2)
                    m_pSelected[i]->level = 0;
            }
            m_bDirty = true;
            return true;
        }
    }

    return false;
}

/*
==============
Editor::Select_DrawAirjob

If the airjob is selected, draw a selection box around it
==============
*/
void Editor::Select_DrawAirjob(airjob_t *aj)
{
    sprite_t *spr;
    float x, y;
    float x1, y1, x2, y2;

    if (IsSelected(aj)) {
        spr = aj->type->sprite;

        m_level.LevelToScreen(aj->x, aj->y, &x, &y);
        x1 = x + spr->mins[0] - 3;
        y1 = y + spr->mins[1] - 3;
        x2 = x + spr->maxs[0] + 3;
        y2 = y + spr->maxs[1] + 3;

        hal->DrawLine(x1, y1, x1+10, y1, 255, 192, 0);
        hal->DrawLine(x1, y1, x1, y1+10, 255, 192, 0);

        hal->DrawLine(x2, y1, x2-10, y1, 255, 192, 0);
        hal->DrawLine(x2, y1, x2, y1+10, 255, 192, 0);

        hal->DrawLine(x2, y2, x2-10, y2, 255, 192, 0);
        hal->DrawLine(x2, y2, x2, y2-10, 255, 192, 0);

        hal->DrawLine(x1, y2, x1+10, y2, 255, 192, 0);
        hal->DrawLine(x1, y2, x1, y2-10, 255, 192, 0);
    }
}

//============================================================================
// node manipulation (not an independent state)

/*
==============
Editor::Node_Action

Bring up the action dialog box
==============
*/
void Editor::Node_Action(airjob_t *aj, int idx, int x, int y)
{
    NodeActionDlg dlg(this, x, y, idx >= 0);
    jobnode_t *jn, *next;
    int code;

    SetMode(em_none);

    code = dlg.Run();

    if (code == NODE_DELETE && idx >= 0) {
        if (idx+1 < aj->numnodes) {
            jn = &aj->nodes[idx];
            next = jn+1;

            next->dx += jn->dx;
            next->dy += jn->dy;
        }
        m_level.DelAirjobNode(aj, idx);
        m_bDirty = true;
    } else if (code == NODE_LEVEL) {
        aj->level++;
        if (aj->level > 2)
            aj->level = 0;

        m_bDirty = true;
    } else if (code == NODE_ADD) {
        idx++;
        jn = m_level.AddAirjobNode(aj, idx, 0, 0);

        if (idx+1 < aj->numnodes)
            next = &aj->nodes[idx+1];
        else
            next = 0;

        if (next) {
            jn->dx = next->dx/2;
            jn->dy = next->dy/2;
            next->dx -= jn->dx;
            next->dy -= jn->dy;
        } else {
            jn->dx = 0;
            jn->dy = 128;
        }

        m_bDirty = true;
    }
}

/*
==============
Editor::Node_MouseButton

RClick: add/remove node
==============
*/
bool Editor::Node_MouseButton(int btn, int x, int y, bool down)
{
    airjob_t *aj;
    int idx;
    float lx, ly;

    if (btn == 1 && down)
    {
        m_level.ScreenToLevel(x, y, &lx, &ly);
        aj = m_level.NodeAtPoint(lx, ly, &idx);
        if (!aj)
            return false;

        Node_Action(aj, idx, x, y);
        return true;
    }
    else if (btn == 0)
    {
        if (down) {
            m_level.ScreenToLevel(x, y, &lx, &ly);
            aj = m_level.NodeAtPoint(lx, ly, &idx);
            if (!aj || idx < 0) // can't move root node
                return false;

            SetMode(em_none);

            m_pNodeDragJob = aj;
            m_iNodeDragIdx = idx;
            m_flNodeDragX = lx;
            m_flNodeDragY = ly;
            m_bNodeDrag = true;
            return true;
        } else {
            if (m_bNodeDrag) {
                m_bNodeDrag = false;
                return true;
            }
        }
    }

    return false;
}

/*
==============
Editor::Node_MouseMove

Actually move a node when in drag mode
==============
*/
void Editor::Node_MouseMove(int x, int y)
{
    jobnode_t *jn;
    float lx, ly;
    float dx, dy;

    if (!m_bNodeDrag)
        return;

    m_level.ScreenToLevel(x, y, &lx, &ly);

    dx = lx - m_flNodeDragX;
    dy = ly - m_flNodeDragY;
    SnapToGrid(&dx, &dy);
    m_flNodeDragX += dx;
    m_flNodeDragY += dy;

    if (!dx && !dy)
        return;

    m_bDirty = true;

    // now move the thing
    jn = &m_pNodeDragJob->nodes[m_iNodeDragIdx];
    jn->dx += dx;
    jn->dy -= dy;

    SnapToGrid(&jn->dx, &jn->dy);

    if (m_iNodeDragIdx+1 >= m_pNodeDragJob->numnodes)
        return;

    jn++;
    jn->dx -= dx;
    jn->dy += dy;

    SnapToGrid(&jn->dx, &jn->dy);
}

#endif