--Copyright (c) 2014~2020, Byrthnoth
--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 <addon name> 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 <your name> 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.

_addon.name = 'Translate'
_addon.version = '2.0.0.0'
_addon.author = 'Byrth'
_addon.commands = {'trans','translate'}


language = 'english'
trans_list = {}
res = require 'resources'
require 'sets'
require 'lists'
require 'pack'
require 'strings'
require 'katakana_to_romanji'
search_comment = {ts = 0, reg = L{}, translated = false}

handled_resources = S{
    'ability_recasts',
    'auto_translates',
    'buffs',
    'days',
    'elements',
    'items',
    'job_abilities',
    'job_traits',
    'job_points',
    'jobs',
    'key_items',
    'merit_points',
    'monster_abilities',
    'monster_skills',
    'monstrosity',
    'moon_phases',
    'mounts',
    'races',
    'regions',
    'skills',
    'spells',
    'titles',
    'weapon_skills',
    'weather',
    'zones'
    }

green_open = string.char(0xEF,0x27)
red_close = string.char(0xEF,0x28)

green_col = ''--string.char(0x1E,2)
rcol = ''--string.char(0x1E,1)

local temp_str

function to_a_code(num)
    local first_byte,second_byte = math.floor(num/256),num%256
    if first_byte == 0 or second_byte == 0 then return nil end
    return string.char(0xFD,2,2,first_byte,second_byte,0xFD):escape()
    -- 0xFD,2,2,8,37,0xFD :: 37 = %
end

function to_item_code(id)
    local first_byte,second_byte = math.floor(id/256),id%256
    local t = 0x07
    if first_byte == 0 then
        t = 0x09
        first_byte = 0xFF
    elseif second_byte == 0 then
        t = 0x0A
        second_byte = 0xFF
    end
    return string.char(0xFD,t,2,first_byte,second_byte,0xFD):escape()
end

function to_ki_code(id)
    local first_byte,second_byte = math.floor(id/256),id%256
    local t = 0x13
    if first_byte == 0 then
        t = 0x15
        first_byte = 0xFF
    elseif second_byte == 0 then
        t = 0x16
        second_byte = 0xFF
    end
    return string.char(0xFD,t,2,first_byte,second_byte,0xFD):escape()
end

function sanity_check(ja)
    return (ja and string.len(ja) > 0 and ja ~= '%.')
end

function load_dict(dict)
    for _,res_line in pairs(dict) do
        local jp = windower.to_shift_jis(res_line.ja or ''):escape()
        local jps = windower.to_shift_jis(res_line.jas or ''):escape()
        if sanity_check(jp) and sanity_check(res_line.en) and jp~= res_line.en:escape() then
            trans_list[jp] = green_col..res_line.en..rcol
        end
        if sanity_check(jps) and sanity_check(res_line.ens) and jps ~= res_line.ens:escape() then
            trans_list[jps] = green_col..res_line.ens..rcol
        end
    end
end

load_dict(katakana_to_romanji)

for res_name in pairs(handled_resources) do
    local resource = res[res_name] or {}
    if res_name == 'auto_translates' then
        for autotranslate_code,res_line in pairs(resource) do
            local jp = windower.to_shift_jis(res_line.ja or ''):escape()
            if sanity_check(jp) and not trans_list[jp] and jp ~= res_line.en:escape() then
                trans_list[jp] = to_a_code(autotranslate_code)
            end
        end
    elseif res_name == 'items' then
        for id,res_line in pairs(resource) do
            local jp = windower.to_shift_jis(res_line.ja or ''):escape()
            if sanity_check(jp) and not trans_list[jp] and jp ~= res_line.en:escape() then
                trans_list[jp] = to_item_code(id)
            end
        end
    elseif res_name == 'key_items' then
        for id,res_line in pairs(resource) do
            local jp = windower.to_shift_jis(res_line.ja or ''):escape()
            if sanity_check(jp) and not trans_list[jp] and jp ~= res_line.en:escape() then
                trans_list[jp] = to_ki_code(id)
            end
        end
    elseif res_name == 'jobs' then
        for _,res_line in pairs(resource) do
            local jp = windower.to_shift_jis(res_line.ja or ''):escape()
            local jps = windower.to_shift_jis(res_line.jas or ''):escape()
            if sanity_check(jp) and sanity_check(res_line.en) and jp ~= res_line.en:escape() then
                trans_list[jp] = green_col..res_line.en..rcol:escape()
            end
            if sanity_check(jps) and sanity_check(res_line.ens) and jp ~= res_line.en:escape() and jps ~= res_line.ens:escape() and res_line.ens ~= 'PUP' then
                trans_list[jps] = green_col..res_line.ens..rcol:escape()
            end
        end
    else
        for _,res_line in pairs(resource) do
            local jp = windower.to_shift_jis(res_line.ja or ''):escape()
            local jps = windower.to_shift_jis(res_line.jas or ''):escape()
            if sanity_check(jp) and not trans_list[jp] and sanity_check(res_line.en) and jp ~= res_line.en:escape() then
                trans_list[jp] = green_col..res_line.en..rcol:escape()
            end
            if sanity_check(jps) and not trans_list[jps] and sanity_check(res_line.ens) and jp ~= res_line.en:escape() and jps ~= res_line.ens:escape() then
                trans_list[jps] = green_col..res_line.ens..rcol:escape()
            end
        end
    end
end

local custom_dict_names = S(windower.get_dir(windower.addon_path..'dicts/')):filter(string.endswith-{'.lua'}):map(string.sub-{1, -5})
for dict_name in pairs(custom_dict_names) do
    local dict = dofile(windower.addon_path..'dicts/'..dict_name..'.lua')
    if dict then
        load_dict(dict)
    end
end

function print_bytes(str)
    local c = ''
    local i = 1
    while i <= #str do
        c = c..' '..str:byte(i)
        i = i + 1
    end
    return c
end

trans_list[string.char(0x46)] = nil
trans_list['\.'] = nil


windower.register_event('incoming chunk',function(id,orgi,modi,is_injected,is_blocked)
    if id == 0x17 and not is_injected and not is_blocked then
        local out_text = modi:unpack('z',0x18)
        
        out_text = translate_phrase(out_text)
        
        if not out_text then return end
        
        if show_original then windower.add_to_chat(8,modi:sub(9,0x17):unpack('z',1)..'[Original]: '..modi:unpack('z',0x18)) end
        while #out_text > 0 do
            local boundary = get_boundary_length(out_text,151)
            local len = math.ceil((boundary+1+23)/2) -- Make sure there is at least one nul after the string
            local out_pack = string.char(0x17,len)..modi:sub(3,0x17)..out_text:sub(1,boundary)

            -- zero pad it
            while #out_pack < len*2 do
                out_pack = out_pack..string.char(0)
            end
            windower.packets.inject_incoming(0x17,out_pack)
            out_text = out_text:sub(boundary+1)
        end
        return true
    end
    return is_blocked
end)

function translate_phrase(out_text)
    local matches,match_bool = {},false
    local function make_matches(catch)
        -- build a table of matches indexed by their length
        local esc = catch:escape()
        if not sanity_check(esc) then return end
        
        
        
        if not matches[#catch] then
            matches[#catch] = {}
        end
        matches[#catch][#matches[#catch]+1] = esc
        match_bool = true
    end
    for jp,en in pairs(trans_list) do
        out_text:gsub(jp,make_matches)
    end
    
    if not match_bool then return end
    
    local order = {}
    for len,_ in pairs(matches) do
        local c,found = 1,false
        while c <= #order do
            if len > order[c] then
                table.insert(order,c,len)
                found = true
                break
            end
            c = c + 1
        end
        if c > #order then order[c] = len end
    end
    
    for _,ind in ipairs(order) do
        for _,option in ipairs(matches[ind]) do
            out_text = sjis_gsub(out_text,unescape(option),unescape(trans_list[option]))
        end
    end
    return out_text
end

function get_boundary_length(str,limit)
    -- If it is already short enough, return it
    if #str <= limit then return #str end
    
    local lim = 0
    for i= 1,#str do
        local c_byte = str:byte(i)
        if c_byte == 0xFD then
            i = i + 6
        elseif ( (c_byte > 0x7F and c_byte <= 0xA0) or c_byte >= 0xE0) then
            i = i + 2
        else
            i = i + 1
        end
        if i > limit then
            return lim
        else
            lim = i
        end
    end
    
    -- Otherwise, try to pick a spot to split that will not interfere with command codes and such
--[[    local boundary = limit
    for i=limit-5,limit do
        local c_byte = str:byte(i)
        if c_byte ==0xFD then
            -- 0xFD: Autotranslate code, 6 bytes
            boundary = i-1
            break
        elseif c_byte == 0xEF and str:byte(i+1) == 0x27 then
            -- Opening green (
            boundary = i-1
            break
        elseif i == limit and ( (c_byte > 0x7F and c_byte <= 0xA0) or c_byte >= 0xE0) then
            -- Double-byte shift_JIS character
            boundary = i-1
            break
        end
    end
    return boundary]]
end

windower.register_event('addon command', function(...)
    local commands = {...}
    if not commands[1] then return end
    if commands[1]:lower() == 'show' then
        show_original=not show_original
        if show_original then
            print('Translate: Showing the original text line.')
        else
            print('Translate: Hiding the original text line.')
        end
    elseif commands[1]:lower() == 'eval' and commands[2] then
        table.remove(commands,1)
        assert(loadstring(table.concat(commands, ' ')))()
    elseif comamnds[1]:lower() == 'help' then
        print('Translate:')
        print('  show: toggles whether to show the original text (default: hide)')
    end
end)

windower.register_event('incoming text',function(org,mod,ocol,mcol,blk)
    if not blk and ocol == 204 then
        local ret = translate_phrase(mod)
        temp_str = ret or org
        if ret then
            if show_original then
                windower.add_to_chat:schedule(0.3, 8,'[Original]: '..org)
            end
            mod = ret
            if org == temp_str then
                blk = true 
                temp_str = ''
            end
            return blk and blk or mod
        end
    end
end)

function unescape(str)
    return (str:gsub('%%([%%%%^%$%*%(%)%.%+%?%-%]%[])','%1'))
end


-- Two problems with how I currently do this:
-- 1: It is possible to have something like 0x94, (0x92, 0x8B,) 0xE1, which are two JP characters that contain a third.
-- 2: It is possible to have a gsub replace something with an autotranslate code, which then causes a later dictionary
--    option to match part of the replacement.
-- If I solve #1, will #2 be an issue? No, it should not be.

function sjis_gsub(str,pattern,rep)
    if not (type(rep) == 'function' or type(rep) == 'string') then return str end
    local str_len,pat_len,ret_str = string.len(str),string.len(pattern),str
    local i = 1
    while i<=str_len-pat_len+1 do
        local c_byte = str:byte(i)
        if str:sub(i,i+pat_len-1) == pattern then
            if type(rep) == 'function' then
                ret_str = rep(pattern) or str
                -- No recursion for functions at the moment, because this addon doesn't need it
                return
            elseif type(rep) == 'string' then
                if i ~= 1 then
                    -- Not the beginning
                    ret_str = str:sub(1,i-1)..rep
                else
                    -- The beginning
                    ret_str = rep
                end
                if i+pat_len <= str_len-pat_len+1 then
                    -- i == 13, pat_len == 2, str_len == 16
                    -- Match is characters 13 and 14. Could conceivably match again to characters 15 and 16.
                    
                    -- Send the remainder of the string back through recursively.
                    return ret_str..sjis_gsub(str:sub(i+pat_len),pattern,rep)
                elseif i+pat_len <= str_len then
                    -- i == 14, pat_len == 2, str_len == 16
                    -- Match is characters 14 and 15, so 16 can't possibly be a match but needs to be stuck on there
                    return ret_str..str:sub(i+pat_len)
                else
                    -- i == 15, pat_len == 2, str_len == 16
                    -- Match is characters 15 and 16, so no further addition is necessary
                    return ret_str
                end
            end
        elseif c_byte == 0xFD then
            i = i + 6
        elseif ( (c_byte > 0x7F and c_byte <= 0xA0) or c_byte >= 0xE0) then
            i = i + 2
        else
            i = i + 1
        end
    end
    return ret_str
end
