WindowID WindowIDs;
ViewID   ViewIDs;

Array<Window *> Windows;
Array<View *> Views;
Array<Buffer *> Buffers;

// console
BufferID NullBufferID;
ViewID   NullViewID;
WindowID NullWindowID;

// hidden floating window
WindowID DebugWindowID;
ViewID DebugViewID;
BufferID DebugBufferID;

WindowID CommandBarWindowID;
WindowID StatusBarWindowID;
WindowID SearchBarWindowID;
ViewID   SearchViewID;
BufferID SearchBufferID;

WindowID ActiveWindow;
WindowID LastActiveLayoutWindowID;
WindowID ScrollbarSelected = {-1};
WindowID DocumentSelected  = {-1};
WindowID ResizerSelected = {-1};
WindowID ResizerHover = {-1};
Caret    DocumentAnchor;

Buffer *LuaProjectBuffer;
Buffer *LuaConfigBuffer;
Buffer *GCInfoBuffer;
Buffer *EventBuffer;
Buffer *ScratchBuffer;
Buffer *TraceBuffer;
View *TraceView;

String WorkDir;
RandomSeed UniqueBufferNameSeed = {};

// lua
lua_State *LuaState         = NULL;
String16   LuaCommandResult = {};
extern luaL_Reg LuaFunctions[];

// clipboard
BlockArena      ClipboardArena;
String16        SavedClipboardString;
Array<String16> SavedClipboardCarets = {SysAllocator};


String GetUniqueBufferName(String working_dir, String prepend_name, String extension = ".log") {
    Scratch scratch;
    String buffer_name = {};
    for (int i = 1; i < INT_MAX; i += 1) {
        buffer_name = Format(scratch, "%S/%S%d%S", working_dir, prepend_name, i, extension);
        buffer_name = GetAbsolutePath(scratch, buffer_name);
        Buffer *exists = FindBuffer(buffer_name);
        if (!exists && !FileExists(buffer_name)) {
            break;
        }
    }
    buffer_name = Intern(&GlobalInternTable, buffer_name);
    return buffer_name;
}

void InitBuffers() {
    Allocator sys_allocator = GetSystemAllocator();
    Scratch scratch;
    Buffer   *null_buffer   = CreateBuffer(sys_allocator, GetUniqueBufferName(GetWorkingDir(scratch), "console"));
    View     *null_view     = CreateView(null_buffer->id);
    Assert(null_buffer->id == NullBufferID && null_view->id == NullViewID);

    TraceBuffer             = CreateBuffer(sys_allocator, GetUniqueBufferName(WorkDir, "trace"));
    TraceView               = CreateView(TraceBuffer->id);
    GCInfoBuffer            = CreateBuffer(sys_allocator, GetUniqueBufferName(WorkDir, "gc"));
    EventBuffer             = CreateBuffer(sys_allocator, GetUniqueBufferName(WorkDir, "events"));
    ScratchBuffer           = BufferOpenFile(GetUniqueBufferName(WorkDir, "scratch"));

    EventBuffer->no_history = true;
    GCInfoBuffer->no_history = true;
}

inline bool IsDocumentSelectionValid() {
    if (DocumentSelected.id == -1) return false;
    return true;
}

inline bool IsScrollbarSelectionValid() {
    if (ScrollbarSelected.id == -1) return false;
    return true;
}

inline ViewID   AllocViewID(View *view) { return {ViewIDs.id++, view}; }
inline WindowID AllocWindowID(Window *window) { return {WindowIDs.id++, window}; }
inline BufferID AllocBufferID(Buffer *buffer) { return {BufferIDs++, buffer}; }

inline Window *GetWindow(WindowID id, Window *default_window = Windows[0]) {
    For(Windows) {
        if (it->id == id) return it;
    }
    return default_window;
}

inline Buffer *GetBuffer(BufferID id) {
    For(Buffers) {
        if (it->id == id) return it;
    }
    return Buffers[0];
}

inline Buffer *FindBuffer(BufferID id) {
    For(Buffers) {
        if (it->id == id) return it;
    }
    return NULL;
}

inline Buffer *GetBuffer(String name) {
    For(Buffers) {
        if (it->name == name) return it;
    }
    return Buffers[0];
}

inline Buffer *FindBuffer(String name) {
    For(Buffers) {
        if (it->name == name) return it;
    }
    return NULL;
}

inline View *GetView(ViewID id) {
    For(Views) {
        if (it->id == id) return it;
    }
    return Views[0];
}

inline bool    IsNull(Buffer *buffer) { return buffer->id.id == NullBufferID.id; }
inline Window *GetActiveWind() { return GetWindow(ActiveWindow); }

Buffer *CreateBuffer(Allocator allocator, String name, Int size) {
    Buffer *result = AllocBuffer(allocator, name, size);
    Add(&Buffers, result);
    return result;
}

Window *CreateWind() {
    Allocator allocator  = GetSystemAllocator();
    Window *w            = AllocType(allocator, Window);
    w->font              = &PrimaryFont;
    w->visible           = true;
    w->layout            = true;
    w->draw_scrollbar    = StyleDrawScrollbar;
    w->draw_line_numbers = StyleDrawLineNumbers;
    w->draw_line_highlight = true;
    w->id                = AllocWindowID(w);
    w->weight            = 1.0;
    Add(&Windows, w);
    return w;
}

View *CreateView(BufferID active_buffer) {
    Allocator al           = GetSystemAllocator();
    View     *view         = AllocType(al, View);
    view->id               = AllocViewID(view);
    view->active_buffer    = active_buffer;
    view->carets.allocator = al;
    Add(&view->carets, {0, 0});
    Add(&Views, view);
    return view;
}

View *FindView(ViewID view_id, View *default_view = NULL) {
    For(Views) {
        if (it->id == view_id) {
            return it;
        }
    }
    return default_view;
}

View *FindView(BufferID buffer_id, View *default_view = NULL) {
    For(Views) {
        if (it->active_buffer == buffer_id) {
            return it;
        }
    }
    return default_view;
}

Window *FindWindow(ViewID view_id, Window *default_window = NULL) {
    For(Windows) {
        if (it->active_view == view_id) {
            return it;
        }
    }
    return default_window;
}

Window *FindWindow(String buffer_name, Window *default_window = NULL) {
    For(Windows) {
        View   *it_view   = GetView(it->active_view);
        Buffer *it_buffer = GetBuffer(it_view->active_buffer);
        if (it_buffer->name == buffer_name) {
            return it;
        }
    }
    return default_window;
}

Window *FindWindow(BufferID buffer_id) {
    For(Windows) {
        View *view = GetView(it->active_view);
        if (view->active_buffer == buffer_id) return it;
    }
    return NULL;
}

View *FindView(String name, View *default_view = NULL) {
    For(Views) {
        Buffer *buffer = GetBuffer(it->active_buffer);
        if (buffer->name == name) {
            return it;
        }
    }
    return default_view;
}

BSet GetBSet(Window *window) {
    BSet set   = {window};
    set.view   = GetView(set.window->active_view);
    set.buffer = GetBuffer(set.view->active_buffer);
    return set;
}

BSet GetBSet(WindowID window_id) {
    Window *window = GetWindow(window_id);
    BSet    result = GetBSet(window);
    return result;
}

BSet GetActiveSet() {
    Window *window = GetWindow(ActiveWindow);
    return GetBSet(window);
}

BSet GetLastActiveLayoutSet() {
    return GetBSet(LastActiveLayoutWindowID);
}

BSet GetConsoleSet() {
    BSet result = {};
    result.window = GetWindow(NullWindowID);
    result.view = GetView(NullViewID);
    result.buffer = GetBuffer(NullBufferID);
    return result;
}

String Command_GetFilename() {
    BSet set = GetLastActiveLayoutSet();
    return set.buffer->name;
}

String GetDir(Buffer *buffer) {
    String name = ChopLastSlash(buffer->name);
    return name;
}

String Command_GetMainDir() {
    BSet   set  = GetLastActiveLayoutSet();
    String name = ChopLastSlash(set.buffer->name);
    return name;
}

Int ConvertUTF8ToUTF16UnixLine(String string, char16_t *buffer, Int buffer_cap) {
    if (string.len == 0) {
        return 0;
    }

    Int buffer_len = 0;
    Assert(buffer_cap > string.len * 2);
    for (Int i = 0; i < string.len;) {
        if (string.data[i] == '\r') {
            i += 1;
            continue;
        }
        if (string.data[i] == '\t') {
            // @WARNING: DONT INCREASE THE SIZE CARELESSLY, WE NEED TO ADJUST BUFFER SIZE
            for (Int i = 0; i < 4; i += 1) buffer[buffer_len++] = u' ';
            i += 1;
            continue;
        }

        uint32_t    u32    = '?';
        UTF32Result decode = UTF8ToUTF32((uint8_t *)(string.data + i), (int64_t)(string.len - i));
        if (!decode.error) {
            i += decode.advance;
            u32 = decode.out_str;
        } else {
            i += 1;
        }

        UTF16Result encode = UTF32ToUTF16(u32);
        if (!encode.error) {
            for (int64_t encode_i = 0; encode_i < encode.len; encode_i += 1) {
                buffer[buffer_len++] = encode.out_str[encode_i];
                Assert(buffer_len < buffer_cap);
            }
        } else {
            buffer[buffer_len++] = u'?';
        }
    }
    return buffer_len;
}

String16 ToUnixString16(Allocator allocator, String string_) {
    Int      cap             = string_.len * 3;
    char16_t *string16_buffer = AllocArray(allocator, char16_t, cap);
    Int      len             = ConvertUTF8ToUTF16UnixLine(string_, string16_buffer, cap);
    String16 string          = {string16_buffer, len};
    return string;
}

Buffer *BufferOpenFile(String path) {
    Allocator sys_allocator = GetSystemAllocator();
    Scratch   scratch;

    path           = GetAbsolutePath(scratch, path);
    Buffer *buffer = GetBuffer(path);
    if (!IsNull(buffer) || (IsNull(buffer) && buffer->name == path)) {
        return buffer;
    }

    if (!FileExists(path)) {
        buffer = CreateBuffer(sys_allocator, path);
    } else if (IsDir(path)) {
        ReportWarningf("failed to open, it's a directory: %S", path);
        return GetBuffer(NullBufferID);
    } else {
        String string         = ReadFile(scratch, path);
        buffer                = CreateBuffer(sys_allocator, path, string.len * 4 + 4096);
        buffer->len           = ConvertUTF8ToUTF16UnixLine(string, buffer->str, buffer->cap);
        buffer->file_mod_time = GetFileModTime(path);
        UpdateLines(buffer, {}, String16{(char16_t *)buffer->data, buffer->len});
    }

    return buffer;
}

View *OpenBufferView(String name) {
    Buffer *buffer = BufferOpenFile(name);
    View   *view   = CreateView(buffer->id);
    return view;
}

View *WindowOpenBufferView(Window *new_parent_window, String name) {
    View *view = FindView(name);
    if (!view) {
        View *result                   = OpenBufferView(name);
        new_parent_window->active_view = result->id;
        return result;
    }

    Window *window = FindWindow(view->id);
    if (!window) {
        new_parent_window->active_view = view->id;
        return view;
    }
    if (window == new_parent_window) {
        return view;
    }

    Assert(window->active_view.id == view->id.id);
    View *result                   = OpenBufferView(name);
    new_parent_window->active_view = result->id;
    return result;
}

bool ViewIsReferenced(ViewID view) {
    if (view == NullViewID) {
        return true;
    }

    For(Windows) {
        if (it->active_view == view) {
            return true;
        }
    }
    return false;
}

bool BufferIsReferenced(BufferID buffer_id) {
    if (buffer_id == NullBufferID) {
        return true;
    }

    if (FindView(buffer_id)) {
        return true;
    }

    return false;
}

void GarbageCollect() {
    Allocator sys_allocator = GetSystemAllocator();

    For(Buffers) {
        if (it->file_mod_time) {
            int64_t new_file_mod_time = GetFileModTime(it->name);
            if (it->file_mod_time != new_file_mod_time) {
                it->changed_on_disk = true;
                if (it->dirty == false) ReopenBuffer(it);
            }
        }
    }

    IterRemove(Views) {
        IterRemovePrepare(Views);

        Buffer *buffer = GetBuffer(it->active_buffer);
        if (!buffer->garbage) {
            continue;
        }

        bool ref = ViewIsReferenced(it->id);
        if (ref) {
            continue;
        }

        remove_item = true;
        Dealloc(&it->carets);
        Dealloc(sys_allocator, it);
    }

    IterRemove(Buffers) {
        IterRemovePrepare(Buffers);

        if (!it->garbage) {
            continue;
        }

        bool ref = BufferIsReferenced(it->id);
        if (ref) {
            continue;
        }

        remove_item = true;
        DeallocBuffer(it);
    }

    IterRemove(Windows) {
        IterRemovePrepare(Windows);
        if (it->kill) {
            Dealloc(sys_allocator, it);
            remove_item = true;
        }
   }
}
