/*
 *  Copyright (c) 2000-2022 Inria
 *  All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions are met:
 *
 *  * Redistributions of source code must retain the above copyright notice,
 *  this list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright notice,
 *  this list of conditions and the following disclaimer in the documentation
 *  and/or other materials provided with the distribution.
 *  * Neither the name of the ALICE Project-Team nor the names of its
 *  contributors may be used to endorse or promote products derived from this
 *  software without specific prior written permission.
 * 
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 *  POSSIBILITY OF SUCH DAMAGE.
 *
 *  Contact: Bruno Levy
 *
 *     https://www.inria.fr/fr/bruno-levy
 *
 *     Inria,
 *     Domaine de Voluceau,
 *     78150 Le Chesnay - Rocquencourt
 *     FRANCE
 *
 */

#include <geogram_gfx/gui/console.h>
#include <geogram_gfx/gui/application.h>
#include <geogram_gfx/imgui_ext/icon_font.h>
#include <geogram/basic/string.h>
#include <geogram/basic/command_line.h>

namespace GEO {

    Console::Console(bool* visible_flag) :
	command_prompt_(true),
        visible_flag_(visible_flag),
        completion_callback_(nullptr),
        history_callback_(nullptr),
        history_index_(0),
        max_history_index_(0)
    {
	input_buf_[0] = '\0';
	scroll_to_bottom_ = 0;
    }

    void Console::notify_error(const std::string& err) {
	geo_argused(err);
	return;
    }
    
    void Console::div(const std::string& value) {
        this->printf("========== %s", value.c_str());
    }
        
    void Console::out(const std::string& value) {
        this->printf("    %s", value.c_str());
    }
        
    void Console::warn(const std::string& value) {
        this->printf("[W] %s", value.c_str());
        if(visible_flag_ != nullptr) {
            *visible_flag_ = true;
        }
    }
        
    void Console::err(const std::string& value) {
        this->printf("[E] %s", value.c_str());
        if(visible_flag_ != nullptr) {
            *visible_flag_ = true;
        }
	notify_error(value);
    }

    void Console::status(const std::string& value) {
	// Do not display error messages twice.
	if(String::string_starts_with(value, "Error:")) {
	    return;
	}
	this->printf("[status] %s", value.c_str());
    }
    
    void Console::clear() {
        buf_.clear();
        line_offsets_.clear();
    }
    
    void Console::printf(const char* fmt, ...) {
        va_list args;
        va_start(args, fmt);
        int old_size = buf_.size();
        buf_.appendfv(fmt, args); 
        va_end(args);
        for (int new_size = buf_.size(); old_size < new_size; old_size++) {
            if (buf_[old_size] == '\n') {
                line_offsets_.push_back(old_size);
            }
        }
	// If I just use a boolean as in ImGuiDemo, then when printing to a
	// hidden console then showing it again does not scroll to bottom
	// (it seems that ImGui needs a couple of frames to 'figure out'
	// what happened).
        scroll_to_bottom_ = 10;
	update();
    }

    void Console::update() {
	if(Application::instance() != nullptr) {
	    Application::instance()->draw();
	}
    }

    static int TextEditCallbackStub(ImGuiInputTextCallbackData* data) {
        Console* console = (Console*)data->UserData;
	return console->TextEditCallback(data);
    }


    int Console::TextEditCallback(ImGuiInputTextCallbackData* data)  {
        switch (data->EventFlag) {
        case ImGuiInputTextFlags_CallbackCompletion: {
	    if(completion_callback_ != nullptr) {
		static const char*
		    completer_word_break_characters = " .(){},+-*/=";
		// Locate beginning of current word
		const char* word_end = data->Buf + data->CursorPos;
		const char* word_start = word_end;
		while (word_start > data->Buf)
		{
		    const char c = word_start[-1];
		    if(strchr(completer_word_break_characters,c) != nullptr) {
			break;
		    }
		    word_start--;
		}  
		index_t startw = index_t(word_start - data->Buf);
		index_t endw   = index_t(word_end - data->Buf);
		std::string cmpword(word_start, size_t(word_end - word_start));
		std::vector<std::string> matches;
		completion_callback_(
		    this, std::string(data->Buf),
		    startw, endw, cmpword, matches
		);
		if(matches.size() == 0) {
		    this->printf("Completions: no match\n");
		} else if(matches.size() == 1) {
		    // Single match. Delete the beginning of the word and
		    // replace it entirely so we've got nice casing
                    data->DeleteChars(
			(int)(word_start-data->Buf),
			(int)(word_end-word_start)
		    );
                    data->InsertChars(data->CursorPos, matches[0].c_str());
		} else {
		    // Several matches, find longest common prefix
		    std::string longest_prefix;
		    size_t cur_char = 0;
		    bool finished = false;
		    while(!finished) {
			char c = '\0';
			for(size_t i=0; i<matches.size(); ++i) {
			    if(
				cur_char >= matches[i].length() ||
				(i != 0 && matches[i][cur_char] != c)
			    ) {
				finished = true;
				break;
			    }
			    c = matches[i][cur_char];
			}
			if(!finished) {
			    longest_prefix.push_back(c);
			}
			++cur_char;
		    }
		    // Replace edited text with longest prefix
		    if(longest_prefix.length() != 0) {
			data->DeleteChars(
			    (int)(word_start-data->Buf),
			    (int)(word_end-word_start)
			);
			data->InsertChars(
			    data->CursorPos, longest_prefix.c_str()
			);
		    }
		    this->printf("Completions:\n");
		    for(size_t i=0; i<matches.size(); ++i) {
			this->printf(
			    "[%d] ... %s\n", int(i), matches[i].c_str()
			);
		    }
		}
	    }
	} break;
        case ImGuiInputTextFlags_CallbackHistory: {
	    if(history_callback_ != nullptr) {
		std::string history_command;
		//   Call the callback first, to give it the opportunity to
		// declare the history size.
		history_callback_(this, history_index_, history_command);
		if(max_history_index_ > 0) {
		    int h = int(history_index_);
		    if(data->EventKey == ImGuiKey_UpArrow) {
			--h;
			if(h < 0) {
			    h = int(max_history_index_);
			}
		    } else if(data->EventKey == ImGuiKey_DownArrow) {
			++h;
			if(h > int(max_history_index_)) {
			    h = 0;
			}
		    }
		    {
			history_index_ = index_t(h);
			if(history_index_ == max_history_index_) {
			    history_command = "";
			} else {
			    history_callback_(
				this, history_index_, history_command
			    );
			}
			int newpos = std::min(
			    data->BufSize-1, int(history_command.length())
			);
			strncpy(
			    data->Buf, history_command.c_str(), size_t(newpos)
			);
			data->Buf[newpos] = '\0';
			data->CursorPos = newpos;
			data->SelectionStart = newpos;
			data->SelectionEnd = newpos;
			data->BufTextLen = newpos;
			data->BufDirty = true;   
		    }
		}
	    }
        } break;
	}
        return 0;
    }

    bool Console::exec_command(const char* command) {
	geo_argused(command);
	// Note: history_index_ and max_history_index_ are
	// not managed here. They are managed by the callback.
	return false;
    }
    
    void Console::draw(bool* visible, bool with_window) {
	if(!*visible) {
	    return;
	}
	if(with_window) {
	    ImGui::Begin("Console", visible);
	}

	bool copy = false;

	bool phone_screen = CmdLine::get_arg_bool("gui:phone_screen");
	bool toolbar_visible = phone_screen;
	
	if(toolbar_visible) {
	    // Add a close button under Android 	
	    if(phone_screen) {
		if (ImGui::SimpleButton(icon_UTF8("window-close").c_str())) {
		    *visible = false;
		}
		ImGui::SameLine();
	    }
	    if (ImGui::SimpleButton(icon_UTF8("eraser").c_str())) {
		clear();
	    }
	    ImGui::Tooltip("clear");
	    ImGui::SameLine();
	    copy = ImGui::SimpleButton(icon_UTF8("copy").c_str());
	    ImGui::Tooltip("copy");	
	    ImGui::SameLine();
	    filter_.Draw((icon_UTF8("filter")+" Filter").c_str(), -200.0f);
	    ImGui::Separator();
	}
	
	if(phone_screen) {
	    // Use smaller font if using phone in vertical mode.
	    if(
		Application::instance() != nullptr &&
		Application::instance()->get_height() >
		Application::instance()->get_width()
	    ) {
		ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[3]);
	    } else {
	    	ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[1]); 
	    }
	} else {
	    ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[1]);	    	    
	}
	
	float scaling = ImGui::GetIO().FontDefault->FontSize / 16.0f;
	
        ImGui::BeginChild(
	    "scrolling",
	    (command_prompt_?ImVec2(0.0f,-20.0f*scaling):ImVec2(0.0f, 0.0f)),
	    false,
            ImGuiWindowFlags_HorizontalScrollbar
        );
	
        if (copy) {
            ImGui::LogToClipboard();
        }
            
        {
            const char* buf_begin = buf_.begin();
            const char* line = buf_begin;
            for (int line_no = 0; line != nullptr; line_no++) {
                const char* line_end =
                    (line_no < line_offsets_.Size) ?
                    buf_begin + line_offsets_[line_no] : nullptr;
                
                if (!filter_.IsActive() ||
		    filter_.PassFilter(line, line_end)
		) {
		    bool is_error = ((line_end - line) >= 3 &&
			line[0] == '[' &&
			line[2] == ']' &&
			(line[1] == 'E' || line[1] == 'W')
		    );
		    if(is_error) {
			ImGui::PushStyleColor(
			    ImGuiCol_Text,ImVec4(1.0f,0.0f,0.0f,1.0f)
			);
		    }
                    ImGui::TextUnformatted(line, line_end);
		    if(is_error) {
			ImGui::PopStyleColor();
		    }
                }
                line = line_end && line_end[1] ? line_end + 1 : nullptr;
            }
        }
	// If I just use a boolean as in ImGuiDemo, then when printing to a
	// hidden console then showing it again does not scroll to bottom
	// (it seems that ImGui needs a couple of frames to 'figure out'
	// what happened).
        if (scroll_to_bottom_ > 0) {
	    ImGui::SetScrollHereY(1.0);
	    --scroll_to_bottom_;
        }
        ImGui::EndChild();
	ImGui::PopFont();

	if(command_prompt_) {
	    ImGui::Text("%s",icon_UTF8("terminal").c_str());
	    ImGui::SameLine();
	    ImGui::PushItemWidth(-20);
	    ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[1]);
	    if(ImGui::InputText(
		   "##CommandInput", input_buf_, geo_imgui_string_length,
		   ImGuiInputTextFlags_EnterReturnsTrue |
		   ImGuiInputTextFlags_CallbackCompletion |
		   ImGuiInputTextFlags_CallbackHistory,
		   &TextEditCallbackStub, (void*)this)
	    ) {
		char* input_end = input_buf_+strlen(input_buf_);
		while (input_end > input_buf_ && input_end[-1] == ' ') {
		    input_end--;
		}
		*input_end = 0;
		if (input_buf_[0]) {
		    exec_command(input_buf_);
		}
		strcpy(input_buf_, "");
	    }
	    ImGui::PopItemWidth();
	    // Keeping auto focus on the input box
	    if (ImGui::IsItemHovered()) {
		ImGui::SetKeyboardFocusHere(-1); // Auto focus previous widget
	    }
	    ImGui::PopFont();
	}

	if(with_window) {
	    ImGui::End();
	}
    }
}
