-- Simple binary patch with IDA dif file support.

local _ENV = mkmodule('binpatch')

local function load_patch(name)
    local filename = name
    if not string.match(filename, '[./\\]') then
        filename = dfhack.getHackPath()..'/patches/'..dfhack.getDFVersion()..'/'..name..'.dif'
    end

    local file, err = io.open(filename, 'r')
    if not file then
        if string.match(err, ': No such file or directory') then
            return nil, 'patch not found'
        end
    end

    local old_bytes = {}
    local new_bytes = {}
    local has_bytes = false

    for line in file:lines() do
        if string.match(line, '^%x+:') then
            local offset, oldv, newv = string.match(line, '^(%x+):%s*(%x+)%s+(%x+)%s*$')
            if not offset then
                file:close()
                return nil, 'could not parse: '..line
            end

            offset, oldv, newv = tonumber(offset,16), tonumber(oldv,16), tonumber(newv,16)
            if oldv > 255 or newv > 255 then
                file:close()
                return nil, 'invalid byte values: '..line
            end

            old_bytes[offset] = oldv
            new_bytes[offset] = newv
            has_bytes = true
        end
    end

    file:close()
    if not has_bytes then
        return nil, 'no patch bytes found'
    end
    return { name = name, old_bytes = old_bytes, new_bytes = new_bytes }
end

local function rebase_table(input)
    local output = {}
    local base = dfhack.internal.getImageBase()
    for k,v in pairs(input) do
        local offset = dfhack.internal.adjustOffset(k)
        if not offset then
            return nil, string.format('invalid offset: %x', k)
        end
        output[base + offset] = v
    end
    return output
end

local function rebase_patch(patch)
    local nold, err = rebase_table(patch.old_bytes)
    if not nold then return nil, err end
    local nnew, err = rebase_table(patch.new_bytes)
    if not nnew then return nil, err end
    return { name = patch.name, old_bytes = nold, new_bytes = nnew }
end

BinaryPatch = defclass(BinaryPatch)

BinaryPatch.ATTRS {
    name = DEFAULT_NIL,
    old_bytes = DEFAULT_NIL,
    new_bytes = DEFAULT_NIL,
}

function load_dif_file(name)
    local patch, err = load_patch(name)
    if not patch then return nil, err end

    local rpatch, err = rebase_patch(patch)
    if not rpatch then return nil, err end

    return BinaryPatch(rpatch)
end

function BinaryPatch:status()
    local old_ok, err, addr = dfhack.internal.patchBytes({}, self.old_bytes)
    if old_ok then
        return 'removed'
    elseif dfhack.internal.patchBytes({}, self.new_bytes) then
        return 'applied'
    else
        return 'conflict', addr
    end
end

function BinaryPatch:isApplied()
    return dfhack.internal.patchBytes({}, self.new_bytes)
end

function BinaryPatch:apply()
    local ok, err, addr = dfhack.internal.patchBytes(self.new_bytes, self.old_bytes)
    if ok then
        return true, 'applied the patch'
    elseif dfhack.internal.patchBytes({}, self.new_bytes) then
        return true, 'patch is already applied'
    else
        return false, string.format('conflict at address %x', addr)
    end
end

function BinaryPatch:isRemoved()
    return dfhack.internal.patchBytes({}, self.old_bytes)
end

function BinaryPatch:remove()
    local ok, err, addr = dfhack.internal.patchBytes(self.old_bytes, self.new_bytes)
    if ok then
        return true, 'removed the patch'
    elseif dfhack.internal.patchBytes({}, self.old_bytes) then
        return true, 'patch is already removed'
    else
        return false, string.format('conflict at address %x', addr)
    end
end

return _ENV
