#include <inttypes.h>
#include <stdlib.h>
#include "window.h"


//================| Window Class Implementation |================//

//Here's a quick, crappy pseudo-RNG since you probably don't have one
uint8_t pseudo_rand_8() {

    static uint16_t seed = 0;
    return (uint8_t)(seed = (12657 * seed + 12345) % 256);
}

//Window constructor
Window* Window_new(int16_t x, int16_t y, uint16_t width,
                   uint16_t height, uint16_t flags, Context* context) {

    //Try to allocate space for a new WindowObj and fail through if malloc fails
    Window* window;
    if(!(window = (Window*)malloc(sizeof(Window))))
        return window;

    //Attempt to initialize the new window
    if(!Window_init(window, x, y, width, height, flags, context)) {
    
        free(window);
        return (Window*)0;
    }

    return window;
}

//Seperate object allocation from initialization so we can implement
//our inheritance scheme
int Window_init(Window* window, int16_t x, int16_t y, uint16_t width,
                uint16_t height, uint16_t flags, Context* context) {

    //Moved over here from the desktop 
    //Create child list or clean up and fail
    if(!(window->children = List_new()))
        return 0;

    //Assign the property values
    window->x = x;
    window->y = y;
    window->width = width;
    window->height = height;
    window->context = context;
    window->flags = flags;
    window->parent = (Window*)0;
    window->drag_child = (Window*)0;
    window->drag_off_x = 0;
    window->drag_off_y = 0;
    window->last_button_state = 0;
    window->paint_function = 0;
    window->mousedown_function = 0;
    window->rmousedown_function = 0;
    window->mousemove_function = 0;
    window->rmouseup_function = 0;
    window->mouseup_function = 0;
    window->keydown_function = 0;
    window->mouseleave_function = 0;
    window->mouseenter_function = 0;
    window->active_child = (Window*)0;
    window->title = (char*)0;
  
    return 1;
}

//Recursively get the absolute on-screen x-coordinate of this window
int Window_screen_x(Window* window) {

    if(window->parent)
        return window->x + Window_screen_x(window->parent);
    
    return window->x;
}

//Recursively get the absolute on-screen y-coordinate of this window
int Window_screen_y(Window* window) {

    if(window->parent)
        return window->y + Window_screen_y(window->parent);
    
    return window->y;
}

void Window_draw_border(Window* window) {

}

//Apply clipping for window bounds without subtracting child window rects
void Window_apply_bound_clipping(Window* window, int in_recursion, List* dirty_regions) {

    Rect *temp_rect, *current_dirty_rect, *clone_dirty_rect;
    int screen_x, screen_y, i;
    List* clip_windows;
    Window* clipping_window;

    //Can't do this without a context
    if(!window->context)
        return;

    //Build the visibility rectangle for this window
    //If the window is decorated and we're recursing, we want to limit
    //the window's drawable area to the area inside the window decoration.
    //If we're not recursing, however, it means we're about to paint 
    //ourself and therefore we want to wait until we've finished painting
    //the window border to shrink the clipping area 
    screen_x = Window_screen_x(window);
    screen_y = Window_screen_y(window);
    
    temp_rect = Rect_new(screen_y, screen_x, screen_y + window->height - 1,
                             screen_x + window->width - 1);

    //If there's no parent (meaning we're at the top of the window tree)
    //then we just add our rectangle and exit
    //Here's our change: If we were passed a dirty region list, we first
    //clone those dirty rects into the clipping region and then intersect
    //the top-level window bounds against it so that we're limited to the
    //dirty region from the outset
    if(!window->parent) {

        if(dirty_regions) {

            //Clone the dirty regions and put them into the clipping list
            for(i = 0; i < dirty_regions->count; i++) {
            
                //Clone
                current_dirty_rect = (Rect*)List_get_at(dirty_regions, i);
                clone_dirty_rect = Rect_new(current_dirty_rect->top,
                                            current_dirty_rect->left,
                                            current_dirty_rect->bottom,
                                            current_dirty_rect->right);
                
                //Add
                Context_add_clip_rect(window->context, clone_dirty_rect);
            }

            //Finally, intersect this top level window against them
            Context_intersect_clip_rect(window->context, temp_rect);

        } else {

            Context_add_clip_rect(window->context, temp_rect);
        }

        return;
    }

    //Otherwise, we first reduce our clipping area to the visibility area of our parent
    Window_apply_bound_clipping(window->parent, 1, dirty_regions);

    //Now that we've reduced our clipping area to our parent's clipping area, we can
    //intersect our own bounds rectangle to get our main visible area  
    Context_intersect_clip_rect(window->context, temp_rect);

    //And finally, we subtract the rectangles of any siblings that are occluding us 
    clip_windows = Window_get_windows_above(window->parent, window);

    while(clip_windows->count) {
        
        clipping_window = (Window*)List_remove_at(clip_windows, 0);

        //Get a rectangle from the window, subtract it from the clipping 
        //region, and dispose of it
        screen_x = Window_screen_x(clipping_window);
        screen_y = Window_screen_y(clipping_window);

        temp_rect = Rect_new(screen_y, screen_x,
                             screen_y + clipping_window->height - 1,
                             screen_x + clipping_window->width - 1);
        Context_subtract_clip_rect(window->context, temp_rect);
        free(temp_rect);
    }

    //Dispose of the used-up list 
    free(clip_windows);
}

void Window_update_title(Window* window) {

}

//Request a repaint of a certain region of a window
void Window_invalidate(Window* window, int top, int left, int bottom, int right) {

    List* dirty_regions;
    Rect* dirty_rect;

    //This function takes coordinates in terms of window coordinates
    //So we need to convert them to screen space 
    int origin_x = Window_screen_x(window);
    int origin_y = Window_screen_y(window);
    top += origin_y;
    bottom += origin_y;
    left += origin_x;
    right += origin_x;
    
    //Attempt to create a new dirty rect list 
    if(!(dirty_regions = List_new()))
        return;

    if(!(dirty_rect = Rect_new(top, left, bottom, right))) {

        free(dirty_regions);
        return;
    }

    if(!List_add(dirty_regions, dirty_rect)) {

        free(dirty_regions);
        free(dirty_rect);
        return;
    }

    Window_paint(window, dirty_regions, 0);

    //Clean up the dirty rect list
    List_remove_at(dirty_regions, 0);
    free(dirty_regions);
    free(dirty_rect); 
}

//Another override-redirect function
void Window_paint(Window* window, List* dirty_regions, uint8_t paint_children) {

    int i, j, screen_x, screen_y, child_screen_x, child_screen_y;
    Window* current_child;
    Rect* temp_rect;

    //Can't paint without a context
    if(!window->context)
        return;

    //Start by limiting painting to the window's visible area
    Window_apply_bound_clipping(window, 0, dirty_regions);

    //Set the context translation
    screen_x = Window_screen_x(window);
    screen_y = Window_screen_y(window);

    //Then subtract the screen rectangles of any children 
    //NOTE: We don't do this in Window_apply_bound_clipping because, due to 
    //its recursive nature, it would cause the screen rectangles of all of 
    //our parent's children to be subtracted from the clipping area -- which
    //would eliminate this window. 
    for(i = 0; i < window->children->count; i++) {

        current_child = (Window*)List_get_at(window->children, i);

        child_screen_x = Window_screen_x(current_child);
        child_screen_y = Window_screen_y(current_child);

        temp_rect = Rect_new(child_screen_y, child_screen_x,
                             child_screen_y + current_child->height - 1,
                             child_screen_x + current_child->width - 1);
        Context_subtract_clip_rect(window->context, temp_rect);
        free(temp_rect);
    }

    //Finally, with all the clipping set up, we can set the context's 0,0 to the top-left corner
    //of the window's drawable area, and call the window's final paint function 
    window->context->translate_x = screen_x;
    window->context->translate_y = screen_y;
    if (window->paint_function) {
        window->paint_function(window, window->paint_function_args);
    }

    //Now that we're done drawing this window, we can clear the changes we made to the context
    Context_clear_clip_rects(window->context);
    window->context->translate_x = 0;
    window->context->translate_y = 0;
    
    //Even though we're no longer having all mouse events cause a redraw from the desktop
    //down, we still need to call paint on our children in the case that we were called with
    //a dirty region list since each window needs to be responsible for recursively checking
    //if its children were dirtied 
    if(!paint_children)
        return;

    for(i = 0; i < window->children->count; i++) {

        current_child = (Window*)List_get_at(window->children, i);

        if(dirty_regions) {

            //Check to see if the child is affected by any of the
            //dirty region rectangles
            for(j = 0; j < dirty_regions->count; j++) {
            
                temp_rect = (Rect*)List_get_at(dirty_regions, j);
                
                screen_x = Window_screen_x(current_child);
                screen_y = Window_screen_y(current_child);

                if(temp_rect->left <= (screen_x + current_child->width - 1) &&
                   temp_rect->right >= screen_x &&
                   temp_rect->top <= (screen_y + current_child->height - 1) &&
                   temp_rect->bottom >= screen_y)
                    break;
            }

            //Skip drawing this child if no intersection was found
            if(j == dirty_regions->count)
                continue;
        }

        //Otherwise, recursively request the child to redraw its dirty areas
        Window_paint(current_child, dirty_regions, 1);
    }
}

//This is the default paint method for a new window
void Window_paint_handler(Window* window, void* v) {

    //Fill in the window background
    Context_fill_rect(window->context, 0, 0,
                      window->width, window->height, WIN_BGCOLOR);
}

//Used to get a list of windows overlapping the passed window
List* Window_get_windows_above(Window* parent, Window* child) {

    int i;
    Window* current_window;
    List* return_list;

    //Attempt to allocate the output list
    if(!(return_list = List_new()))
        return return_list;

    //We just need to get a list of all items in the
    //child list at higher indexes than the passed window
    //We start by finding the passed child in the list
    for(i = 0; i < parent->children->count; i++)
        if(child == (Window*)List_get_at(parent->children, i))
            break;

    //Now we just need to add the remaining items in the list
    //to the output (IF they overlap, of course)
    //NOTE: As a bonus, this will also automatically fall through
    //if the window wasn't found
    for(i++; i < parent->children->count; i++) {

        current_window = List_get_at(parent->children, i);

        //Our good old rectangle intersection logic
        if(current_window->x <= (child->x + child->width - 1) &&
		   (current_window->x + current_window->width - 1) >= child->x &&
		   current_window->y <= (child->y + child->height - 1) &&
		   (current_window->y + current_window->height - 1) >= child->y)
            List_add(return_list, current_window); //Insert the overlapping window
    }

    return return_list; 
}

//Used to get a list of windows which the passed window overlaps
//Same exact thing as get_windows_above, but goes backwards through
//the list. Could probably be made a little less redundant if you really wanted
List* Window_get_windows_below(Window* parent, Window* child) {

    int i;
    Window* current_window;
    List* return_list;

    //Attempt to allocate the output list
    if(!(return_list = List_new()))
        return return_list;

    //We just need to get a list of all items in the
    //child list at higher indexes than the passed window
    //We start by finding the passed child in the list
    for(i = parent->children->count - 1; i > -1; i--)
        if(child == (Window*)List_get_at(parent->children, i))
            break;

    //Now we just need to add the remaining items in the list
    //to the output (IF they overlap, of course)
    //NOTE: As a bonus, this will also automatically fall through
    //if the window wasn't found
    for(i--; i > -1; i--) {

        current_window = List_get_at(parent->children, i);

        //Our good old rectangle intersection logic
        if(current_window->x <= (child->x + child->width - 1) &&
		   (current_window->x + current_window->width - 1) >= child->x &&
		   current_window->y <= (child->y + child->height - 1) &&
		   (current_window->y + current_window->height - 1) >= child->y)
            List_add(return_list, current_window); //Insert the overlapping window
    }

    return return_list; 
}

//Breaking 
void Window_raise(Window* window, uint8_t do_draw) {

    int i;
    Window *parent, *last_active;

    if(!window->parent)
        return;

    parent = window->parent;

    if(parent->active_child == window)
        return;

    last_active = parent->active_child;

    //Find the child in the list
    for(i = 0; i < parent->children->count; i++)
        if((Window*)List_get_at(parent->children, i) == window)
            break;

    List_remove_at(parent->children, i); //Pull window out of list
    List_add(parent->children, (void*)window); //Insert at the top
  
    //Make it active 
    parent->active_child = window;
   
    //Do a redraw if it was requested
    if(!do_draw)
        return;

    Window_paint(window, (List*)0, 1);

    //Make sure the old active window gets an updated title color 
    Window_update_title(last_active);
}

//We're wrapping this guy so that we can handle any needed redraw
void Window_move(Window* window, int new_x, int new_y) {
    int old_x = window->x;
    int old_y = window->y;
    Rect new_window_rect;
    List *replacement_list, *dirty_list, *dirty_windows;

    //To make life a little bit easier, we'll make the not-unreasonable 
    //rule that if a window is moved, it must become the top-most window
    Window_raise(window, 0); //Raise it, but don't repaint it yet

    //We'll hijack our dirty rect collection from our existing clipping operations
    //So, first we'll get the visible regions of the original window position
    Window_apply_bound_clipping(window, 0, (List*)0);

    //Temporarily update the window position
    window->x = new_x;
    window->y = new_y;

    //Calculate the new bounds
    new_window_rect.top = Window_screen_y(window);
    new_window_rect.left = Window_screen_x(window);
    new_window_rect.bottom = new_window_rect.top + window->height - 1;
    new_window_rect.right = new_window_rect.left + window->width - 1;

    //Reset the window position
    window->x = old_x;
    window->y = old_y;

    //Now, we'll get the *actual* dirty area by subtracting the new location of
    //the window 
    Context_subtract_clip_rect(window->context, &new_window_rect);

    //Now that the context clipping tools made the list of dirty rects for us,
    //we can go ahead and steal the list it made for our own purposes
    //(yes, it would be cleaner to spin off our boolean rect functions so that
    //they can be used both here and by the clipping region tools, but I ain't 
    //got time for that junk)
    if(!(replacement_list = List_new())) {

        Context_clear_clip_rects(window->context);
        return;
    }

    dirty_list = window->context->clip_rects;
    window->context->clip_rects = replacement_list;

    //Now, let's get all of the siblings that we overlap before the move
    dirty_windows = Window_get_windows_below(window->parent, window);

    window->x = new_x;
    window->y = new_y;

    //And we'll repaint all of them using the dirty rects
    //(removing them from the list as we go for convenience)
    while(dirty_windows->count)
        Window_paint((Window*)List_remove_at(dirty_windows, 0), dirty_list, 1);

    //The one thing that might still be dirty is the parent we're inside of
    Window_paint(window->parent, dirty_list, 0);

    //We're done with the lists, so we can dump them
    while(dirty_list->count)
        free(List_remove_at(dirty_list, 0));

    free(dirty_list);
    free(dirty_windows);

    //With the dirtied siblings redrawn, we can do the final update of 
    //the window location and paint it at that new position
    Window_paint(window, (List*)0, 1);
}

//Interface between windowing system and mouse device
void Window_process_mouse(Window* window, uint16_t mouse_x,
    uint16_t mouse_y, uint8_t mouse_buttons)
{
    if (!window) return;

    int i, inner_x1, inner_y1, inner_x2, inner_y2;
    Window* child = NULL;

    for (i = window->children->count - 1; i >= 0; i--) {

        child = (Window*) List_get_at(window->children, i);
        if (!child) continue;

        if (!(mouse_x >= child->x && mouse_x < (child->x + child->width) &&
            mouse_y >= child->y && mouse_y < (child->y + child->height))) {

            //TODO: only do this when the mouse actually leaves

            if (child->mouseleave_function) {
                child->mouseleave_function(child, child->paint_function_args);
            }

            continue;
        }

        Window_process_mouse(child, mouse_x - child->x, mouse_y - child->y, mouse_buttons);
        break;
    }

    if (window->rmouseup_function && !(mouse_buttons & 2)) {
        window->rmouseup_function(window, window->paint_function_args, mouse_x, mouse_y);
    }
    if (window->rmousedown_function && (mouse_buttons & 2)) {
        window->rmousedown_function(window, window->paint_function_args, mouse_x, mouse_y);
    }

    if (window->mouseup_function && !(mouse_buttons & 1)) {
        window->mouseup_function(window, window->paint_function_args, mouse_x, mouse_y);
    }
    if (window->mousedown_function && (mouse_buttons & 1)) {
        window->mousedown_function(window, window->paint_function_args, mouse_x, mouse_y);
    }
    if (window->mousemove_function) {
        window->mousemove_function(window, window->paint_function_args, mouse_x, mouse_y);
    }

    window->last_button_state = mouse_buttons;
}

void Window_process_key(Window* window, KeyStates key)
{
    if (!window) return;

    if (window->keydown_function) {
        window->keydown_function(window, window->paint_function_args, key);
    }

    for (int i = window->children->count - 1; i >= 0; i--) {
        Window* child = (Window*) List_get_at(window->children, i);
        if (child) {
            Window_process_key(child, key);
        }
    }
}

//The default handler for window mouse events doesn't do anything
void Window_mousedown_handler(Window* window, int x, int y) {
 
    return;
}

void Window_update_context(Window* window, Context* context) {

    int i;

    window->context = context;

    for(i = 0; i < window->children->count; i++)
        Window_update_context((Window*)List_get_at(window->children, i), context);
}

//Quick wrapper for shoving a new entry into the child list
void Window_insert_child(Window* window, Window* child) {

    child->parent = window;
    List_add(window->children, child);
    child->parent->active_child = child;
    
    Window_update_context(child, window->context);
}

void Window_remove_child(Window* window, Window* child)
{
    for (int i = 0; i < window->children->count; ++i) {
        Window* w = (Window*) List_get_at(window->children, i);
        if (w == child) {
            int cx = w->x;
            int cy = w->y;
            int cw = w->width;
            int ch = w->height;
            List_remove_at(window->children, i);
            Window_invalidate(window, cx, cy, ch, cw);
            return;
        }
    }
}


//A method to automatically create a new window in the provided parent window
Window* Window_create_window(Window* window, int16_t x, int16_t y,  
                             uint16_t width, int16_t height, uint16_t flags) {

    //Attempt to create the window instance
    Window* new_window;
    if(!(new_window = Window_new(x, y, width, height, flags, window->context)))
        return new_window;

    //Attempt to add the window to the end of the parent's children list
    //If we fail, make sure to clean up all of our allocations so far 
    if(!List_add(window->children, (void*)new_window)) {

        free(new_window);
        return (Window*)0;
    }

    //Set the new child's parent 
    new_window->parent = window;
    new_window->parent->active_child = new_window;

    return new_window;
}

//Assign a string to the title of the window
void Window_set_title(Window* window, char* new_title) {

    int len, i;

    //Make sure to free any preexisting title 
    if(window->title) {

        for(len = 0; window->title[len]; len++);
        free(window->title);
    }

    //We don't have strlen, so we're doing this manually
    for(len = 0; new_title[len]; len++);

    //Try to allocate new memory to clone the string
    //(+1 because of the trailing zero in a c-string)
    if(!(window->title = (char*)malloc((len + 1) * sizeof(char))))
        return;

    //Clone the passed string into the window's title
    //Including terminating zero
    for(i = 0; i <= len; i++)
        window->title[i] = new_title[i];

    Window_update_title(window);
}

//Add the characters from the passed string to the end of the window title
void Window_append_title(Window* window, char* additional_chars) {

    char* new_string;
    int original_length, additional_length, i;

    //Set the title if there isn't already one
    if(!window->title) {

        Window_set_title(window, additional_chars);
        return;
    }

    //Get the length of the original string
    for(original_length = 0; window->title[original_length]; original_length++);

    //Get the length of the new string
    for(additional_length = 0; additional_chars[additional_length]; additional_length++);

    //Try to malloc a new string of the needed size
    if(!(new_string = (char*)malloc(sizeof(char) * (original_length + additional_length + 1)))) {
        return;
    }

    //Copy the base string into the new string
    for(i = 0; window->title[i]; i++)
        new_string[i] = window->title[i];

    //Copy the appended chars into the new string
    for(i = 0; additional_chars[i]; i++)
        new_string[original_length + i] = additional_chars[i];

    //Add the final zero char
    new_string[original_length + i] = 0;

    //And swap the string pointers
    free(window->title);
    window->title = new_string;

    Window_update_title(window);
}
