-------------------------------------------------------------------------------
--                                                                           --
-- D2K: A Doom Source Port for the 21st Century                              --
--                                                                           --
-- Copyright (C) 2014: See COPYRIGHT file                                    --
--                                                                           --
-- This file is part of D2K.                                                 --
--                                                                           --
-- D2K is free software: you can redistribute it and/or modify it under the  --
-- terms of the GNU General Public License as published by the Free Software --
-- Foundation, either version 2 of the License, or (at your option) any      --
-- later version.                                                            --
--                                                                           --
-- D2K is distributed in the hope that it will be useful, but WITHOUT ANY    --
-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --
-- FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more    --
-- details.                                                                  --
--                                                                           --
-- You should have received a copy of the GNU General Public License along   --
-- with D2K.  If not, see <http://www.gnu.org/licenses/>.                    --
--                                                                           --
-------------------------------------------------------------------------------

local class = require('middleclass')
local lgi = require('lgi')
local Cairo = lgi.cairo
local GLib = lgi.GLib
local Pango = lgi.Pango
local PangoCairo = lgi.PangoCairo
local InputInterface = require('input_interface')

TextWidget = class('TextWidget', InputInterface.InputInterface)

local ALIGN_LEFT       = 0
local ALIGN_CENTER     = 1
local ALIGN_RIGHT      = 2
local ALIGN_JUSTIFY    = 3

local ALIGN_TOP        = 0
local ALIGN_BOTTOM     = 2

local WRAP_NONE        = 0
local WRAP_WORD        = 1
local WRAP_CHAR        = 2
local WRAP_WORD_CHAR   = 3

local ELLIPSIZE_NONE   = 0
local ELLIPSIZE_START  = 1
local ELLIPSIZE_MIDDLE = 2
local ELLIPSIZE_END    = 3

function TextWidget:initialize(tw)
    tw = tw or {}

    InputInterface.InputInterface.initialize(self, tw)

    if tw['height_in_lines'] ~= nil and tw['height'] ~= nil then
        error(string.format(
            '%s: Cannot specify both height_in_lines and height',
            self:get_name()
        ))
    end

    self.layout_needs_rebuilding = false

    self.layout_changed = false
    self.display_changed = false
    self.content_changed = false

    self.layout_pixel_width = nil
    self.layout_pixel_height = nil
    self.layout_line_count = nil
    self.layout_ink_extents = nil
    self.layout_logical_extents = nil

    self.text = tw.text or ''
    -- self.max_width = tw.max_width or tw.width or 0
    -- self.max_height = tw.max_height or tw.height or 0
    self.outline_color = tw.outline_color or {0.0, 0.0, 0.0, 0.0}
    self.outline_text = tw.outline_text or false
    self.outline_width = tw.outline_width or 0

    if tw['height_in_lines'] ~= nil then
        self:set_height_in_lines(tw.height_in_lines)
    end

    self.scrollable = tw.scrollable or false
    self.use_markup = tw.use_markup or false
    self.strip_ending_newline = tw.strip_ending_newline or false

    self.text_context = nil
    self.current_render_context = nil
    self.layout = nil

    self.horizontal_offset = 0.0
    self.vertical_offset = 0.0

    self.get_external_text = nil
    self.external_text_updated = nil
    self.clear_external_text_updated = nil

    self:set_word_wrap(tw.word_wrap or WRAP_NONE)
    self:set_horizontal_alignment(tw.horizontal_alignment or ALIGN_LEFT)
    self:set_vertical_alignment(tw.vertical_alignment or ALIGN_TOP)
    self:set_ellipsize(tw.ellipsize or ELLIPSIZE_NONE)

    self:layout_text()
end

function TextWidget:get_text()
    return self.text
end

function TextWidget:set_text(text)
    if self.get_external_text then
        error(string.format("%s: Can't set text when displaying external text",
            self:get_name()
        ))
    end

    if not GLib.utf8_validate(text) then
        print(debug.traceback())
        print('bad text passed to text widget');
        return
    end

    self.text = text

    self:handle_content_change()
end

function TextWidget:get_fg_color()
    return self.fg_color
end

function TextWidget:set_fg_color(fg_color)
    self.fg_color = fg_color
    self:handle_display_change()
end

function TextWidget:get_bg_color()
    return self.bg_color
end

function TextWidget:set_bg_color(bg_color)
    self.bg_color = bg_color
    self:handle_display_change()
end

function TextWidget:get_outline_color()
    return self.outline_color
end

function TextWidget:set_outline_color(outline_color)
    self.outline_color = outline_color
    self:handle_display_change()
end

function TextWidget:get_outline_text()
    return self.outline_text
end

function TextWidget:set_outline_text(outline_text)
    self.outline_text = outline_text
    self:handle_display_change()
end

function TextWidget:get_outline_width()
    return self.outline_width
end

function TextWidget:set_outline_width(outline_width)
    self.outline_width = outline_width
    self:handle_display_change()
end

function TextWidget:set_height_in_lines(height_in_lines)
    self:set_pixel_height(height_in_lines * self:get_font_height())
    self:handle_dimension_change()
end

function TextWidget:get_scrollable()
      return self.scrollable
end

function TextWidget:set_scrollable(scrollable)
    self.scrollable = scrollable
end

function TextWidget:get_use_markup()
    return self.use_markup
end

-- CG: TODO: Probably shouldn't be able to switch a widget between markup and
--           non-markup.  *Feels* like a job for a mixin, too.

function TextWidget:set_use_markup(use_markup)
    self.use_markup = use_markup
end

function TextWidget:get_strip_ending_newline()
    return self.strip_ending_newline
end

function TextWidget:set_strip_ending_newline(strip_ending_newline)
    self.strip_ending_newline = strip_ending_newline
    self:handle_content_change()
end

function TextWidget:get_text_context()
    return self.text_context
end

function TextWidget:set_text_context(text_context)
    self.text_context = text_context
end

function TextWidget:get_current_render_context()
    return self.current_render_context
end

function TextWidget:set_current_render_context(current_render_context)
    self.current_render_context = current_render_context
end

function TextWidget:set_font(font)
    InputInterface.InputInterface.set_font(self, font)

    self:get_layout():set_font_description(Pango.FontDescription.from_string(
        self:get_font():get_description()
    ))
end

function TextWidget:check_layout()
    local cr = d2k.overlay.render_context
    local current_render_context = self:get_current_render_context()

    if not self.layout or
       not current_render_context or
       not current_render_context == cr then
        self:build_layout()
    end
end

function TextWidget:get_layout()
    self:check_layout()
    return self.layout
end

function TextWidget:get_horizontal_offset()
    return self.horizontal_offset
end

function TextWidget:set_horizontal_offset(horizontal_offset)
    self.horizontal_offset = horizontal_offset
    self:check_offsets()
    self:handle_display_change()
end

function TextWidget:get_vertical_offset()
    return self.vertical_offset
end

function TextWidget:set_vertical_offset(vertical_offset)
    self.vertical_offset = vertical_offset
    self:check_offsets()
    self:handle_display_change()
end

function TextWidget:layout_text()
    local text = self:get_text()

    if self:get_strip_ending_newline() then
        while true do
            local text_length = #text

            if text:sub(text_length, text_length) ~= '\n' then
                break
            end

            text = text:sub(1, text_length - 1)
        end
    end

    if self:get_use_markup() then
        self:get_layout():set_markup(text, -1)
    else
        self:get_layout():set_text(text, -1)
    end

    PangoCairo.update_context(
        d2k.overlay.render_context, d2k.overlay.text_context
    )
    PangoCairo.update_layout(d2k.overlay.render_context, self:get_layout())
    self:check_offsets()

    self:update_layout_pixel_size()
    self:update_layout_line_count()
    self:update_layout_extents()

    self.content_changed = false
end

function TextWidget:print_lines()
    local iter = self:get_layout():get_iter()

    repeat
        local line = iter:get_line_readonly()
        local ink_rect, line_rect = iter:get_line_extents()

        line_rect.x      = line_rect.x / Pango.SCALE
        line_rect.y      = line_rect.y / Pango.SCALE
        line_rect.width  = line_rect.width / Pango.SCALE
        line_rect.height = line_rect.height / Pango.SCALE

        print(string.format('{%s, %s, %s, %s}',
            line_rect.x,
            line_rect.y,
            line_rect.width,
            line_rect.height
        ))
    until not iter:next_line()
end

function TextWidget:tick()
    self:check_layout()
    if self.get_external_text and self.external_text_updated() then
        local external_text = self.get_external_text()

        if external_text ~= nil then
            self.text = self.text .. self.get_external_text()
        end

        self:handle_content_change()
        self.clear_external_text_updated()
    end
end

function TextWidget:begin_render()
    InputInterface.InputInterface.begin_render(self)
    self:check_layout()
    if self.content_changed then
        self:layout_text()
    end
end

function TextWidget:render()
    local cr = d2k.overlay.render_context
    local fg_color = self:get_fg_color()
    local bg_color = self:get_bg_color()
    local outline_color = self:get_outline_color()
    local lw = self:get_layout_pixel_width()
    local lh = self:get_layout_pixel_height()
    local line_count = self:get_layout_line_count()

    cr:save()

    cr:set_operator(Cairo.Operator.OVER)

    cr:set_source_rgba(bg_color[1], bg_color[2], bg_color[3], bg_color[4])
    cr:paint()

    if line_count <= 0 then
        cr:restore()
        return
    end

    cr:set_source_rgba(fg_color[1], fg_color[2], fg_color[3], fg_color[4])

    local lx = 0
    local ly = 0
    local text_width = self:get_pixel_width() - (
        self:get_left_padding() + self:get_right_padding()
    )
    local text_height = self:get_pixel_height() - (
        self:get_top_padding() + self:get_bottom_padding()
    )

    if self.horizontal_alignment == ALIGN_LEFT then
        lx = self:get_x() + self:get_left_padding()
    elseif self.horizontal_alignment == ALIGN_CENTER then
        lx = self:get_x() + (self:get_pixel_width() / 2) - (lw / 2)
    elseif self.horizontal_alignment == ALIGN_RIGHT then
        lx = self:get_x() + self:get_pixel_width() - (
            lw + self:get_left_padding()
        )
    end

    if self.vertical_alignment == ALIGN_TOP then
        ly = self:get_y() + self:get_top_padding()
    elseif self.vertical_alignment == ALIGN_CENTER then
        ly = self:get_y() + (self:get_pixel_height() / 2) - (lh / 2)
    elseif self.vertical_alignment == ALIGN_BOTTOM then
        ly = self:get_y() + self:get_pixel_height() - (
            lh + self:get_top_padding()
        )
    end

    local layout_ink_extents = self:get_layout_ink_extents()
    local layout_logical_extents = self:get_layout_logical_extents()

    lx = lx - self:get_horizontal_offset()
    ly = ly - self:get_vertical_offset()

    local iter = self:get_layout():get_iter()
    local line_number = 1
    local min_line = 0
    local max_line = 0
    local start_y_offset = 0
    local rendered_at_least_one_line = false

    repeat
        local line_baseline_pixels = iter:get_baseline() / Pango.SCALE
        local line_end_y = line_baseline_pixels + ly
        local should_render_line = false
        local should_quit_after_rendering = false

        if line_end_y > 0 then
            should_render_line = true
        end

        if should_render_line then
            local line = iter:get_line_readonly()
            local line_ink_extents, line_logical_extents =
                iter:get_line_extents()
            local line_logical_x = line_logical_extents.x / Pango.SCALE
            local line_logical_y = line_logical_extents.y / Pango.SCALE
            local line_logical_height =
                line_logical_extents.height / Pango.SCALE
            local line_start_x = line_logical_x + lx
            local line_start_y = line_logical_y + ly

            if (line_start_y - self:get_y()) >= text_height then
                should_quit_after_rendering = true
            end

            rendered_at_least_one_line = true

            cr:move_to(
                line_start_x, line_baseline_pixels + ly + start_y_offset
            )

            if self:get_outline_text() then
                cr:save()

                PangoCairo.layout_line_path(cr, line)

                cr:save()
                cr:set_line_width(self:get_outline_width())
                cr:set_source_rgba(
                    outline_color[1],
                    outline_color[2],
                    outline_color[3],
                    outline_color[4]
                )
                cr:stroke_preserve()
                cr:restore()

                cr:set_source_rgba(
                    fg_color[1], fg_color[2], fg_color[3], fg_color[4]
                )
                cr:fill_preserve()

                cr:restore()
            else
                PangoCairo.show_layout_line(cr, line)
            end

            if should_quit_after_rendering then
                break
            end
        end

        line_number = line_number + 1
    until not iter:next_line()

    cr:restore()
end

function TextWidget:end_render()
    InputInterface.InputInterface.end_render(self)
    self.layout_changed = false
    self.content_changed = false
end

function TextWidget:build_layout()
    local word_wrap = self:get_word_wrap()

    self:set_current_render_context(d2k.overlay.render_context)

    self.layout = Pango.Layout.new(d2k.overlay.text_context)

    self:get_layout():set_font_description(Pango.FontDescription.from_string(
        self:get_font():get_description()
    ))

    if word_wrap == WRAP_NONE then
        self.layout:set_width(-1)
    elseif word_wrap == WRAP_WORD then
        self.layout:set_width(self:get_pixel_width() * Pango.SCALE)
        self.layout:set_wrap(Pango.WrapMode.WORD)
    elseif word_wrap == WRAP_CHAR then
        self.layout:set_width(self:get_pixel_width() * Pango.SCALE)
        self.layout:set_wrap(Pango.WrapMode.CHAR)
    elseif word_wrap == WRAP_WORD_CHAR then
        self.layout:set_width(self:get_pixel_width() * Pango.SCALE)
        self.layout:set_wrap(Pango.WrapMode.WORD_CHAR)
    end

    self:handle_layout_change()
    self:handle_display_change()
end

-- CG: TODO: This should be set only during initialization

function TextWidget:set_external_text_source(get_text,
                                             text_updated,
                                             clear_text_updated)
    self.get_external_text = get_text
    self.external_text_updated = text_updated
    self.clear_external_text_updated = clear_text_updated
end

function TextWidget:clear()
    if self.get_external_text then
        error(string.format(
            "%s: Can't clear text when displaying external text",
            self.name
        ))
    end
    self:set_text('')
end

function TextWidget:reset()
    InputInterface.InputInterface.reset(self)
    self:build_layout()
end

function TextWidget:get_word_wrap()
    return self.word_wrap
end

function TextWidget:set_word_wrap(word_wrap)
    local layout = self:get_layout()

    if word_wrap == WRAP_NONE then
        self.word_wrap = WRAP_NONE
    elseif word_wrap == WRAP_WORD then
        self.word_wrap = WRAP_WORD
    elseif word_wrap == WRAP_CHAR then
        self.word_wrap = WRAP_CHAR
    elseif word_wrap == WRAP_WORD_CHAR then
        self.word_wrap = WRAP_WORD_CHAR
    else
        error(string.format(
            'TextWidget:set_word_wrap: Invalid word wrap value %s',
            word_wrap
        ))
    end

    self:build_layout()
end

function TextWidget:is_wrapped()
    return self:get_layout():is_wrapped()
end

function TextWidget:get_horizontal_alignment()
    return self.horizontal_alignment
end

function TextWidget:set_horizontal_alignment(horizontal_alignment)
    local layout = self:get_layout()

    if horizontal_alignment == ALIGN_LEFT then
        self.horizontal_alignment = ALIGN_LEFT
        layout:set_alignment(Pango.Alignment.LEFT)
    elseif horizontal_alignment == ALIGN_CENTER then
        self.horizontal_alignment = ALIGN_CENTER
        layout:set_alignment(Pango.Alignment.CENTER)
    elseif horizontal_alignment == ALIGN_RIGHT then
        self.horizontal_alignment = ALIGN_RIGHT
        layout:set_alignment(Pango.Alignment.RIGHT)
    elseif horizontal_alignment == ALIGN_JUSTIFY then
        self.horizontal_alignment = ALIGN_JUSTIFY
        layout:set_alignment(Pango.Alignment.JUSTIFY)
    else
        error(string.format(
            'TextWidget:set_horizontal_alignment: Invalid horizontal ' ..
            'alignment %s',
            horizontal_alignment
        ))
    end

    self:handle_layout_change()
end

function TextWidget:get_vertical_alignment()
    return self.vertical_alignment
end

function TextWidget:set_vertical_alignment(vertical_alignment)
    if vertical_alignment == ALIGN_TOP then
        self.vertical_alignment = ALIGN_TOP
    elseif vertical_alignment == ALIGN_CENTER then
        self.vertical_alignment = ALIGN_CENTER
    elseif vertical_alignment == ALIGN_BOTTOM then
        self.vertical_alignment = ALIGN_BOTTOM
    else
        error(string.format(
            'TextWidget:set_vertical_alignment: Invalid vertical alignment %s',
            vertical_alignment
        ))
    end

    self:handle_layout_change()
end

function TextWidget:get_ellipsize()
    return self.ellipsize
end

function TextWidget:set_ellipsize(ellipsize)
    local layout = self:get_layout()

    if ellipsize == ELLIPSIZE_NONE then
        self.ellipsize = ELLIPSIZE_NONE
        layout:set_ellipsize(Pango.EllipsizeMode.NONE)
    elseif ellipsize == ELLIPSIZE_START then
        self.ellipsize = ELLIPSIZE_START
        layout:set_ellipsize(Pango.EllipsizeMode.START)
    elseif ellipsize == ELLIPSIZE_MIDDLE then
        self.ellipsize = ELLIPSIZE_MIDDLE
        layout:set_ellipsize(Pango.EllipsizeMode.MIDDLE)
    elseif ellipsize == ELLIPSIZE_END then
        self.ellipsize = ELLIPSIZE_END
        layout:set_ellipsize(Pango.EllipsizeMode.END)
    else
        error(string.format(
            'TextWidget:set_ellipsize: Invalid ellipsization value %s',
            ellipsize
        ))
    end

    self:handle_layout_change()
end

function TextWidget:is_ellipsized()
    return self:get_layout():is_ellipsized()
end

function TextWidget:get_layout_pixel_width()
    return self.layout_pixel_width
end

function TextWidget:get_layout_pixel_height()
    return self.layout_pixel_height
end

function TextWidget:update_layout_pixel_size()
    local layout_width, layout_height = self:get_layout():get_pixel_size()

    self.layout_pixel_width = layout_width
    self.layout_pixel_height = layout_height
end

function TextWidget:get_layout_line_count()
    return self.layout_line_count
end

function TextWidget:update_layout_line_count()
    self.layout_line_count = self:get_layout():get_line_count()
end

function TextWidget:get_layout_ink_extents()
    return self.layout_ink_extents
end

function TextWidget:get_layout_logical_extents()
    return self.layout_logical_extents
end

function TextWidget:update_layout_extents()
    local ink_extents, logical_extents = self:get_layout():get_pixel_extents()

    self.layout_ink_extents = ink_extents
    self.layout_logical_extents = logical_extents
end

function TextWidget:check_offsets()
    local text_width = self:get_pixel_width() - (
        self:get_left_padding() + self:get_right_padding()
    )
    local text_height = self:get_pixel_height() - (
        self:get_top_padding() + self:get_bottom_padding()
    )
    local layout_width, layout_height = self:get_layout():get_pixel_size()
    local min_x = 0
    local max_x = layout_width - text_width
    local x_delta = max_x - min_x
    local min_y = 0
    local max_y = text_height - self:get_top_padding()
    local y_delta = max_y - min_y

    if self.horizontal_alignment == ALIGN_CENTER then
        min_x = -((layout_width - text_width) / 2) - self:get_left_padding()
        max_x = ((layout_width - text_width) / 2)
    elseif self.horizontal_alignment == ALIGN_RIGHT then
        min_x = -(layout_width - text_width) - self:get_left_padding()
        max_x = 0
    end

    if self.vertical_alignment == ALIGN_CENTER then
        min_y = -((layout_height - text_height) / 2) - self:get_top_padding()
        max_y = ((layout_height - text_height) / 2)
    elseif self.vertical_alignment == ALIGN_BOTTOM then
        min_y = -(layout_height - text_height) - self:get_top_padding()
        max_y = 0
    end

    if layout_width <= text_width then
        min_x = 0
        max_x = 0
    end

    if layout_height <= text_height then
        min_y = 0
        max_y = 0
    end

    if self:get_horizontal_offset() < min_x then
        self.horizontal_offset = min_x
    end

    if self:get_horizontal_offset() > max_x then
        self.horizontal_offset = max_x
    end

    if self:get_vertical_offset() < min_y then
        self.vertical_offset = min_y
    end

    if self:get_vertical_offset() > max_y then
        self.vertical_offset = max_y
    end
end

function TextWidget:scroll_left(pixels)
    self:set_horizontal_offset(self:get_horizontal_offset() - pixels)
    self:check_offsets()
    self:handle_display_change()
end

function TextWidget:scroll_right(pixels)
    self:set_horizontal_offset(self:get_horizontal_offset() + pixels)
    self:check_offsets()
    self:handle_display_change()
end

function TextWidget:scroll_up(pixels)
    self:set_vertical_offset(self:get_vertical_offset() - pixels)
    self:handle_display_change()
end

function TextWidget:scroll_down(pixels)
    self:set_vertical_offset(self:get_vertical_offset() + pixels)
    self:handle_display_change()
end

function TextWidget:write(text)
    if self.get_external_text then
        error(string.format("%s: Can't write text when displaying external text",
            self.name
        ))
    end

    if self:get_use_markup() then
        self:set_text(self:get_text() .. text)
    else
        self:set_text(self:get_text() .. GLib.markup_escape_text(text, -1))
    end
end

function TextWidget:echo(text)
    if self.get_external_text then
        error(string.format("%s: Can't echo text when displaying external text",
            self.name
        ))
    end

    if self:get_use_markup() then
        self:set_text(self:get_text() .. text .. '\n')
    else
        self:set_text(
            self:get_text() .. GLib.markup_escape_text(text, -1) .. '\n'
        )
    end
end

function TextWidget:handle_overlay_reset()
    InputInterface.InputInterface.handle_overlay_reset(self)
    self:handle_layout_change()
    self:handle_content_change()
end

function TextWidget:handle_layout_change()
    self.layout_changed = true
    self:invalidate_render()
end

function TextWidget:handle_content_change()
    self.content_changed = true
    self:invalidate_render()
end

return {
    TextWidget       = TextWidget,
    ALIGN_LEFT       = ALIGN_LEFT,
    ALIGN_CENTER     = ALIGN_CENTER,
    ALIGN_RIGHT      = ALIGN_RIGHT,
    ALIGN_JUSTIFY    = ALIGN_JUSTIFY,
    ALIGN_TOP        = ALIGN_TOP,
    ALIGN_BOTTOM     = ALIGN_BOTTOM,
    WRAP_NONE        = WRAP_NONE,
    WRAP_WORD        = WRAP_WORD,
    WRAP_CHAR        = WRAP_CHAR,
    WRAP_WORD_CHAR   = WRAP_WORD_CHAR,
    ELLIPSIZE_NONE   = ELLIPSIZE_NONE,
    ELLIPSIZE_START  = ELLIPSIZE_START,
    ELLIPSIZE_MIDDLE = ELLIPSIZE_MIDDLE,
    ELLIPSIZE_END    = ELLIPSIZE_END,
}

-- vi: et ts=4 sw=4

