/*
 * Copyright (c) 2019 The Khronos Group Inc.
 * Copyright (c) 2019 Valve Corporation
 * Copyright (c) 2019 LunarG, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Author: Charles Giessen <charles@lunarg.com>
 *
 */

#pragma once

#include <iomanip>
#include <iostream>
#include <ostream>
#include <stack>
#include <sstream>
#include <string>

#include <assert.h>

std::string insert_quotes(std::string s) { return "\"" + s + "\""; }

std::string to_string_16(uint8_t uid[16]) {
    std::stringstream ss;
    ss << std::hex << std::setfill('0');
    for (int i = 0; i < 16; ++i) {
        if (i == 4 || i == 6 || i == 8 || i == 10) ss << '-';
        ss << std::setw(2) << static_cast<unsigned>(uid[i]);
    }
    return ss.str();
}

std::string to_string_8(uint8_t uid[8]) {
    std::stringstream ss;
    ss << std::hex << std::setfill('0');
    for (int i = 0; i < 8; ++i) {
        if (i == 4) ss << '-';
        ss << std::setw(2) << static_cast<unsigned>(uid[i]);
    }
    return ss.str();
}

std::string VkVersionString(uint32_t version) {
    uint32_t major = version >> 22;
    uint32_t minor = (version >> 12) & 0x3ff;
    uint32_t patch = version & 0xfff;
    return std::to_string(major) + "." + std::to_string(minor) + "." + std::to_string(patch);
}

std::string VkVersionString(VulkanVersion v) {
    return std::to_string(v.major) + "." + std::to_string(v.minor) + "." + std::to_string(v.patch);
}

enum class OutputType { text, html, json, vkconfig_output };

class Printer {
  public:
    Printer(OutputType output_type, std::ostream &out, const uint32_t selected_gpu, const VulkanVersion vulkan_version)
        : output_type(output_type), out(out) {
        switch (output_type) {
            case (OutputType::text):
                out << "==========\n";
                out << "VULKANINFO\n";
                out << "==========\n\n";
                out << "Vulkan Instance Version: " << VkVersionString(vulkan_version) << "\n\n\n";

                break;
            case (OutputType::html):
                out << "<!doctype html>\n";
                out << "<html lang='en'>\n";
                out << "\t<head>\n";
                out << "\t\t<title>vulkaninfo</title>\n";
                out << "\t\t<style>\n";
                out << "\t\thtml {\n";
                out << "\t\t\tbackground-color: #0b1e48;\n";
                out << "\t\t\tbackground-image: url(\"https://vulkan.lunarg.com/img/bg-starfield.jpg\");\n";
                out << "\t\t\tbackground-position: center;\n";
                out << "\t\t\t-webkit-background-size: cover;\n";
                out << "\t\t\t-moz-background-size: cover;\n";
                out << "\t\t\t-o-background-size: cover;\n";
                out << "\t\t\tbackground-size: cover;\n";
                out << "\t\t\tbackground-attachment: fixed;\n";
                out << "\t\t\tbackground-repeat: no-repeat;\n";
                out << "\t\t\theight: 100%;\n";
                out << "\t\t}\n";
                out << "\t\t#header {\n";
                out << "\t\t\tz-index: -1;\n";
                out << "\t\t}\n";
                out << "\t\t#header>img {\n";
                out << "\t\t\tposition: absolute;\n";
                out << "\t\t\twidth: 160px;\n";
                out << "\t\t\tmargin-left: -280px;\n";
                out << "\t\t\ttop: -10px;\n";
                out << "\t\t\tleft: 50%;\n";
                out << "\t\t}\n";
                out << "\t\t#header>h1 {\n";
                out << "\t\t\tfont-family: Arial, \"Helvetica Neue\", Helvetica, sans-serif;\n";
                out << "\t\t\tfont-size: 44px;\n";
                out << "\t\t\tfont-weight: 200;\n";
                out << "\t\t\ttext-shadow: 4px 4px 5px #000;\n";
                out << "\t\t\tcolor: #eee;\n";
                out << "\t\t\tposition: absolute;\n";
                out << "\t\t\twidth: 400px;\n";
                out << "\t\t\tmargin-left: -80px;\n";
                out << "\t\t\ttop: 8px;\n";
                out << "\t\t\tleft: 50%;\n";
                out << "\t\t}\n";
                out << "\t\tbody {\n";
                out << "\t\t\tfont-family: Consolas, monaco, monospace;\n";
                out << "\t\t\tfont-size: 14px;\n";
                out << "\t\t\tline-height: 20px;\n";
                out << "\t\t\tcolor: #eee;\n";
                out << "\t\t\theight: 100%;\n";
                out << "\t\t\tmargin: 0;\n";
                out << "\t\t\toverflow: hidden;\n";
                out << "\t\t}\n";
                out << "\t\t#wrapper {\n";
                out << "\t\t\tbackground-color: rgba(0, 0, 0, 0.7);\n";
                out << "\t\t\tborder: 1px solid #446;\n";
                out << "\t\t\tbox-shadow: 0px 0px 10px #000;\n";
                out << "\t\t\tpadding: 8px 12px;\n\n";
                out << "\t\t\tdisplay: inline-block;\n";
                out << "\t\t\tposition: absolute;\n";
                out << "\t\t\ttop: 80px;\n";
                out << "\t\t\tbottom: 25px;\n";
                out << "\t\t\tleft: 50px;\n";
                out << "\t\t\tright: 50px;\n";
                out << "\t\t\toverflow: auto;\n";
                out << "\t\t}\n";
                out << "\t\tdetails>details {\n";
                out << "\t\t\tmargin-left: 22px;\n";
                out << "\t\t}\n";
                out << "\t\tdetails>summary:only-child::-webkit-details-marker {\n";
                out << "\t\t\tdisplay: none;\n";
                out << "\t\t}\n";
                out << "\t\t.var, .type, .val {\n";
                out << "\t\t\tdisplay: inline;\n";
                out << "\t\t}\n";
                out << "\t\t.var {\n";
                out << "\t\t}\n";
                out << "\t\t.type {\n";
                out << "\t\t\tcolor: #acf;\n";
                out << "\t\t\tmargin: 0 12px;\n";
                out << "\t\t}\n";
                out << "\t\t.val {\n";
                out << "\t\t\tcolor: #afa;\n";
                out << "\t\t\tbackground: #222;\n";
                out << "\t\t\ttext-align: right;\n";
                out << "\t\t}\n";
                out << "\t\t</style>\n";
                out << "\t</head>\n";
                out << "\t<body>\n";
                out << "\t\t<div id='header'>\n";
                out << "\t\t\t<h1>vulkaninfo</h1>\n";
                out << "\t\t</div>\n";
                out << "\t\t<div id='wrapper'>\n";

                out << "\t\t\t<details><summary>Vulkan Instance Version: <span class='val'>" << VkVersionString(vulkan_version)
                    << "</span></summary></details>\n\t\t\t<br />\n";
                indents += 3;
                break;
            case (OutputType::json):
                out << "{\n";
                out << "\t\"$schema\": \"https://schema.khronos.org/vulkan/devsim_1_0_0.json#\",\n";
                out << "\t\"comments\": {\n";
                out << "\t\t\"desc\": \"JSON configuration file describing GPU " << selected_gpu
                    << ". Generated using the vulkaninfo program.\",\n";
                out << "\t\t\"vulkanApiVersion\": \"" << VkVersionString(vulkan_version) << "\"\n";
                out << "\t}";
                indents++;
                is_first_item.push(false);
                is_array.push(false);
                break;
            case (OutputType::vkconfig_output):
                out << "{\n";
                out << "\t\"Vulkan Instance Version\": \"" << VkVersionString(vulkan_version) << "\"";
                indents++;
                is_first_item.push(false);
                is_array.push(false);
                break;
            default:
                break;
        }
    }
    ~Printer() {
        switch (output_type) {
            case (OutputType::text):

                break;
            case (OutputType::html):
                out << "\t\t</div>\n";
                out << "\t</body>\n";
                out << "</html>\n";
                indents -= 3;
                break;
            case (OutputType::json):
            case (OutputType::vkconfig_output):
                out << "\n}\n";
                indents--;
                is_first_item.pop();
                assert(is_first_item.empty() && "mismatched number of ObjectStart/ObjectEnd or ArrayStart/ArrayEnd's");
                is_array.pop();
                break;
        }
        assert(indents == 0 && "indents must be zero at program end");
    };

    Printer(const Printer &) = delete;
    const Printer &operator=(const Printer &) = delete;

    OutputType Type() { return output_type; }

    // When an error occurs, call this to create a valid output file. Needed for json/html
    void FinishOutput() {
        switch (output_type) {
            case (OutputType::text):
                indents = 0;
                break;
            case (OutputType::html):
                while (indents > 3) {
                    out << "</details>\n";
                    indents--;
                }
                break;
            case (OutputType::json):
            case (OutputType::vkconfig_output):
                while (indents > 1) {
                    out << "\n" << std::string(static_cast<size_t>(indents), '\t');
                    if (is_array.top()) {
                        out << "]";
                    } else {
                        out << "}";
                    }
                    is_array.pop();
                    indents--;
                }
                break;
        }
    }

    // Custom Formatting
    // use by prepending with p.SetXXX().ObjectStart/ArrayStart

    Printer &SetHeader() {
        set_next_header = true;
        return *this;
    }

    Printer &SetSubHeader() {
        set_next_subheader = true;
        return *this;
    }

    Printer &SetOpenDetails() {
        set_details_open = true;
        return *this;
    }

    Printer &SetTitleAsType() {
        set_object_name_as_type = true;
        return *this;
    }

    Printer &SetAsType() {
        set_as_type = true;
        return *this;
    }

    Printer &SetElementIndex(int index) {
        assert(index >= 0 && "cannot set element index to a negative value");
        element_index = index;
        return *this;
    }

    void ObjectStart(std::string object_name, int32_t count_subobjects = -1) {
        switch (output_type) {
            case (OutputType::text): {
                out << std::string(static_cast<size_t>(indents), '\t') << object_name;
                if (element_index != -1) {
                    out << "[" << element_index << "]";
                }
                out << ":";
                if (count_subobjects >= 0) {
                    out << " count = " << count_subobjects;
                }
                out << "\n";
                size_t headersize = object_name.size() + 1;
                if (count_subobjects >= 0) {
                    headersize += 9 + std::to_string(count_subobjects).size();
                }
                if (element_index != -1) {
                    headersize += 2 + std::to_string(element_index).size();
                    element_index = -1;
                }
                PrintHeaderUnderlines(headersize);
                break;
            }
            case (OutputType::html):
                out << std::string(static_cast<size_t>(indents), '\t');
                if (set_details_open) {
                    out << "<details open>";
                    set_details_open = false;
                } else {
                    out << "<details>";
                }
                out << "<summary>";
                if (set_object_name_as_type) {
                    out << "<span class='type'>" << object_name << "</span>";
                    set_object_name_as_type = false;
                } else {
                    out << object_name;
                }
                if (element_index != -1) {
                    out << "[<span class='val'>" << element_index << "</span>]";
                    element_index = -1;
                }
                if (count_subobjects >= 0) {
                    out << ": count = <span class='val'>" << std::to_string(count_subobjects) << "</span>";
                }
                out << "</summary>\n";
                break;
            case (OutputType::json):
                if (!is_first_item.top()) {
                    out << ",\n";
                } else {
                    is_first_item.top() = false;
                }
                out << std::string(static_cast<size_t>(indents), '\t');
                // Objects with no name are elements in an array of objects
                if (object_name == "" || element_index != -1) {
                    out << "{\n";
                    element_index = -1;
                } else {
                    out << "\"" << object_name << "\": {\n";
                }

                is_first_item.push(true);
                is_array.push(false);

                break;
            case (OutputType::vkconfig_output):
                if (!is_first_item.top()) {
                    out << ",\n";
                } else {
                    is_first_item.top() = false;
                }
                out << std::string(static_cast<size_t>(indents), '\t');

                if (element_index != -1) {
                    out << "\"" << object_name << "[" << element_index << "]\": {\n";
                    element_index = -1;
                } else {
                    out << "\"" << object_name << "\": {\n";
                }
                is_first_item.push(true);
                is_array.push(false);

                break;
            default:
                break;
        }
        indents++;
    }
    void ObjectEnd() {
        indents--;
        assert(indents >= 0 && "indents cannot go below zero");
        switch (output_type) {
            case (OutputType::text):

                break;
            case (OutputType::html):
                out << std::string(static_cast<size_t>(indents), '\t') << "</details>\n";
                break;
            case (OutputType::json):
            case (OutputType::vkconfig_output):
                out << "\n" << std::string(static_cast<size_t>(indents), '\t') << "}";
                is_first_item.pop();
                assert(is_array.top() == false && "cannot call ObjectEnd while inside an Array");
                is_array.pop();
                break;
            default:
                break;
        }
    }
    void ArrayStart(std::string array_name, int32_t element_count = 0) {
        switch (output_type) {
            case (OutputType::text): {
                out << std::string(static_cast<size_t>(indents), '\t') << array_name << ":";
                size_t underline_count = array_name.size() + 1;
                if (element_count >= 0) {
                    out << " count = " << element_count;
                    underline_count += 9 + std::to_string(element_count).size();
                }
                out << "\n";
                PrintHeaderUnderlines(underline_count);
                break;
            }
            case (OutputType::html):
                out << std::string(static_cast<size_t>(indents), '\t');
                if (set_details_open) {
                    out << "<details open>";
                    set_details_open = false;
                } else {
                    out << "<details>";
                }
                out << "<summary>" << array_name;
                if (element_count >= 0) {
                    out << ": count = <span class='val'>" << element_count << "</span>";
                }
                out << "</summary>\n";
                break;
            case (OutputType::json):
            case (OutputType::vkconfig_output):
                if (!is_first_item.top()) {
                    out << ",\n";
                } else {
                    is_first_item.top() = false;
                }
                out << std::string(static_cast<size_t>(indents), '\t') << "\"" << array_name << "\": "
                    << "[\n";
                is_first_item.push(true);
                is_array.push(true);
                break;
            default:
                break;
        }
        indents++;
    }
    void ArrayEnd() {
        indents--;
        assert(indents >= 0 && "indents cannot go below zero");
        switch (output_type) {
            case (OutputType::text):

                break;
            case (OutputType::html):
                out << std::string(static_cast<size_t>(indents), '\t') << "</details>\n";
                break;
            case (OutputType::json):
            case (OutputType::vkconfig_output):
                out << "\n" << std::string(static_cast<size_t>(indents), '\t') << "]";
                is_first_item.pop();
                assert(is_array.top() == true && "cannot call ArrayEnd while inside an Object");
                is_array.pop();
                break;
            default:
                break;
        }
    }

    // For printing key-value pairs.
    // min_key_width lines up the values listed
    // value_description is for reference information and is displayed inside parenthesis after the value
    template <typename T>
    void PrintKeyValue(std::string key, T value, size_t min_key_width = 0, std::string value_description = "") {
        switch (output_type) {
            case (OutputType::text):
                if (min_key_width > key.size()) {
                    out << std::string(static_cast<size_t>(indents), '\t') << key << std::string(min_key_width - key.size(), ' ');
                } else {
                    out << std::string(static_cast<size_t>(indents), '\t') << key;
                }
                out << " = " << value;
                if (value_description != "") {
                    out << " (" << value_description << ")";
                }
                out << "\n";
                break;
            case (OutputType::html):
                out << std::string(static_cast<size_t>(indents), '\t') << "<details><summary>" << key;
                if (min_key_width > key.size()) {
                    out << std::string(min_key_width - key.size(), ' ');
                }
                if (set_as_type) {
                    set_as_type = false;
                    out << " = <span class='type'>" << value << "</span>";
                } else {
                    out << " = <span class='val'>" << value << "</span>";
                }
                if (value_description != "") {
                    out << " (<span class='val'>" << value_description << "</span>)";
                }
                out << "</summary></details>\n";
                break;
            case (OutputType::json):
                if (!is_first_item.top()) {
                    out << ",\n";
                } else {
                    is_first_item.top() = false;
                }
                out << std::string(static_cast<size_t>(indents), '\t') << "\"" << key << "\": " << value;
                break;
            case (OutputType::vkconfig_output):
                if (!is_first_item.top()) {
                    out << ",\n";
                } else {
                    is_first_item.top() = false;
                }
                out << std::string(static_cast<size_t>(indents), '\t') << "\"" << key << "\": ";
                if (value_description != "") {
                    out << "\"" << value << " (" << value_description << ")\"";
                } else {
                    out << value;
                }
            default:
                break;
        }
    }

    // For printing key - string pairs (necessary because of json)
    void PrintKeyString(std::string key, std::string value, size_t min_key_width = 0, std::string value_description = "") {
        switch (output_type) {
            case (OutputType::text):
            case (OutputType::html):
                PrintKeyValue(key, value, min_key_width, value_description);
                break;
            case (OutputType::json):
            case (OutputType::vkconfig_output):
                PrintKeyValue(key, std::string("\"") + value + "\"", min_key_width, value_description);
                break;
            default:
                break;
        }
    }

    // For printing key - string pairs (necessary because of json)
    void PrintKeyBool(std::string key, bool value, size_t min_key_width = 0, std::string value_description = "") {
        switch (output_type) {
            case (OutputType::text):
            case (OutputType::html):
            case (OutputType::vkconfig_output):
                PrintKeyValue(key, value ? "true" : "false", min_key_width, value_description);
                break;
            case (OutputType::json):
                PrintKeyValue(key, value, min_key_width, value_description);
                break;
            default:
                break;
        }
    }

    // print inside array
    template <typename T>
    void PrintElement(T element, std::string value_description = "") {
        switch (output_type) {
            case (OutputType::text):
                out << std::string(static_cast<size_t>(indents), '\t') << element;
                if (value_description != "") {
                    out << " (" << value_description << ")";
                }
                out << "\n";
                break;
            case (OutputType::html):
                out << std::string(static_cast<size_t>(indents), '\t') << "<details><summary>";
                if (set_as_type) {
                    set_as_type = false;
                    out << "<span class='type'>" << element << "</span>";
                } else {
                    out << "<span class='val'>" << element << "</span>";
                }
                if (value_description != "") {
                    out << " (<span class='val'>" << value_description << "</span>)";
                }
                out << "</summary></details>\n";
                break;
            case (OutputType::json):
            case (OutputType::vkconfig_output):
                if (!is_first_item.top()) {
                    out << ",\n";
                } else {
                    is_first_item.top() = false;
                }
                out << std::string(static_cast<size_t>(indents), '\t') << element;
                break;
            default:
                break;
        }
    }
    void PrintString(std::string string, std::string value_description = "") {
        switch (output_type) {
            case (OutputType::text):
            case (OutputType::html):
                PrintElement(string, value_description);
                break;
            case (OutputType::json):
            case (OutputType::vkconfig_output):
                PrintElement("\"" + string + "\"", value_description);
            default:
                break;
        }
    }
    void PrintExtension(std::string ext_name, uint32_t revision, int min_width = 0) {
        switch (output_type) {
            case (OutputType::text):
                out << std::string(static_cast<size_t>(indents), '\t') << ext_name << std::string(min_width - ext_name.size(), ' ')
                    << " : extension revision " << revision << "\n";
                break;
            case (OutputType::html):
                out << std::string(static_cast<size_t>(indents), '\t') << "<details><summary>" << DecorateAsType(ext_name)
                    << std::string(min_width - ext_name.size(), ' ') << " : extension revision "
                    << DecorateAsValue(std::to_string(revision)) << "</summary></details>\n";
                break;
            case (OutputType::json):
                break;
            case (OutputType::vkconfig_output):
                ObjectStart(ext_name);
                PrintKeyValue("specVersion", revision);
                ObjectEnd();
                break;
            default:
                break;
        }
    }
    void AddNewline() {
        if (output_type == OutputType::text) {
            out << "\n";
        }
    }
    void IndentIncrease() {
        if (output_type == OutputType::text) {
            indents++;
        }
    }
    void IndentDecrease() {
        if (output_type == OutputType::text) {
            indents--;
            assert(indents >= 0 && "indents cannot go below zero");
        }
    }

    std::string DecorateAsType(const std::string &input) {
        if (output_type == OutputType::html)
            return "<span class='type'>" + input + "</span>";
        else
            return input;
    }

    std::string DecorateAsValue(const std::string &input) {
        if (output_type == OutputType::html)
            return "<span class='val'>" + input + "</span>";
        else
            return input;
    }

  protected:
    OutputType output_type;
    std::ostream &out;
    int indents = 0;

    // header, subheader
    bool set_next_header = false;
    bool set_next_subheader = false;

    // html coloring
    bool set_as_type = false;

    // open <details>
    bool set_details_open = false;

    // make object titles the color of types
    bool set_object_name_as_type = false;

    // objects which are in an array
    int element_index = -1;  // negative one is the sentinel value

    // json
    std::stack<bool> is_first_item;  // for json: for adding a comma inbetween objects
    std::stack<bool> is_array;       // for json: match pairs of {}'s and []'s

    // utility
    void PrintHeaderUnderlines(size_t length) {
        assert(indents >= 0 && "indents must not be negative");
        assert(length <= 10000 && "length shouldn't be unreasonably large");
        if (set_next_header) {
            out << std::string(static_cast<size_t>(indents), '\t') << std::string(length, '=') << "\n";
            set_next_header = false;
        } else if (set_next_subheader) {
            out << std::string(static_cast<size_t>(indents), '\t') << std::string(length, '-') << "\n";
            set_next_subheader = false;
        }
    }
};
// Purpose: When a Printer starts an object or array it will automatically indent the output. This isn't
// always desired, requiring a manual decrease of indention. This wrapper facilitates that while also
// automatically re-indenting the output to the previous indent level on scope exit.
class IndentWrapper {
  public:
    IndentWrapper(Printer &p) : p(p) {
        if (p.Type() == OutputType::text) p.IndentDecrease();
    }
    ~IndentWrapper() {
        if (p.Type() == OutputType::text) p.IndentIncrease();
    }

  private:
    Printer &p;
};

class ObjectWrapper {
  public:
    ObjectWrapper(Printer &p, std::string object_name) : p(p) { p.ObjectStart(object_name); }
    ObjectWrapper(Printer &p, std::string object_name, size_t count_subobjects) : p(p) {
        p.ObjectStart(object_name, static_cast<int32_t>(count_subobjects));
    }
    ~ObjectWrapper() { p.ObjectEnd(); }

  private:
    Printer &p;
};

class ArrayWrapper {
  public:
    ArrayWrapper(Printer &p, std::string array_name, size_t element_count = 0) : p(p) {
        p.ArrayStart(array_name, static_cast<int32_t>(element_count));
    }
    ~ArrayWrapper() { p.ArrayEnd(); }

  private:
    Printer &p;
};