-- @docclass
ProtocolLogin = extends(Protocol, 'ProtocolLogin')

LoginServerError = 10
LoginServerTokenSuccess = 12
LoginServerCaptchaChallenge = 22  -- Relencia: CAPTCHA challenge (must not conflict with other opcodes)
LoginServerCaptchaPassToken = 23  -- Relencia: CAPTCHA pass token granted after solving
LoginServerTokenError = 13
LoginServerUpdate = 17
LoginServerMotd = 20
LoginServerUpdateNeeded = 30
LoginServerSessionKey = 40
LoginServerCharacterList = 100
LoginServerExtendedCharacterList = 101
LoginServerCastersList = 102  -- Relencia: List of active casters for spectating (empty credentials)

-- Since 10.76
LoginServerRetry = 10
LoginServerErrorNew = 11

function ProtocolLogin:login(host, port, accountName, accountPassword, authenticatorToken, stayLogged, captchaId, captchaAnswer, passToken)
    if string.len(host) == 0 or port == nil or port == 0 then
        signalcall(self.onLoginError, self, tr('You must enter a valid server address and port.'))
        return
    end

    self.host = host
    self.port = port
    self.accountName = accountName
    self.accountPassword = accountPassword
    self.authenticatorToken = authenticatorToken
    self.stayLogged = stayLogged
    self.captchaId = captchaId or 0
    self.captchaAnswer = captchaAnswer or ""
    self.passToken = passToken or ""
    self.connectCallback = self.sendLoginPacket

    self:connect(host, port)
end

function ProtocolLogin:cancelLogin()
    self:disconnect()
end

function ProtocolLogin:sendLoginPacket()
    local msg = OutputMessage.create()
    msg:addU8(ClientOpcodes.ClientEnterAccount)
    msg:addU16(g_game.getOs())

    msg:addU16(g_game.getProtocolVersion())

    if g_game.getFeature(GameClientVersion) then
        msg:addU32(g_game.getClientVersion())
    end

    if g_game.getFeature(GameContentRevision) then
        msg:addU16(g_things.getContentRevision())
        msg:addU16(0)
    else
        msg:addU32(g_things.getDatSignature())
    end
    msg:addU32(g_sprites.getSprSignature())
    msg:addU32(PIC_SIGNATURE)

    if g_game.getFeature(GamePreviewState) then
        msg:addU8(0)
    end

    local offset = msg:getMessageSize()
    if g_game.getFeature(GameLoginPacketEncryption) then
        -- first RSA byte must be 0
        msg:addU8(0)

        -- xtea key
        self:generateXteaKey()
        local xteaKey = self:getXteaKey()
        msg:addU32(xteaKey[1])
        msg:addU32(xteaKey[2])
        msg:addU32(xteaKey[3])
        msg:addU32(xteaKey[4])
    end

    if g_game.getFeature(GameAccountNames) then
        msg:addString(self.accountName)
    else
        msg:addU32(tonumber(self.accountName))
    end

    msg:addString(self.accountPassword)

    -- Always add pass token and CAPTCHA fields (empty if not present) so server can distinguish from RSA padding
    msg:addString(self.passToken or "")  -- CAPTCHA pass token (96-bit hex string)
    msg:addU32(self.captchaId or 0)
    if self.captchaId and self.captchaId > 0 then
        msg:addString(self.captchaAnswer or "")
    end

    if self.getLoginExtendedData then
        local data = self:getLoginExtendedData()
        msg:addString(data)
    end

    local paddingBytes = g_crypt.rsaGetSize() - (msg:getMessageSize() - offset)
    assert(paddingBytes >= 0)
    for i = 1, paddingBytes do
        msg:addU8(math.random(0, 0xff))
    end

    if g_game.getFeature(GameLoginPacketEncryption) then
        msg:encryptRsa()
    end

    if g_game.getFeature(GameOGLInformation) then
        msg:addU8(1) -- unknown
        msg:addU8(1) -- unknown

        if g_game.getClientVersion() >= 1072 then
            msg:addString(string.format('%s %s', g_graphics.getVendor(), g_graphics.getRenderer()))
        else
            msg:addString(g_graphics.getRenderer())
        end
        msg:addString(g_graphics.getVersion())
    end

    -- add RSA encrypted auth token
    if g_game.getFeature(GameAuthenticator) then
        offset = msg:getMessageSize()

        -- first RSA byte must be 0
        msg:addU8(0)
        msg:addString(self.authenticatorToken)

        if g_game.getFeature(GameSessionKey) then
            msg:addU8(booleantonumber(self.stayLogged))
        end

        paddingBytes = g_crypt.rsaGetSize() - (msg:getMessageSize() - offset)
        assert(paddingBytes >= 0)
        for i = 1, paddingBytes do
            msg:addU8(math.random(0, 0xff))
        end

        msg:encryptRsa()
    end

    if g_game.getFeature(GameProtocolChecksum) then
        self:enableChecksum()
    end

    self:send(msg)

    if g_game.getFeature(GameLoginPacketEncryption) then
        self:enableXteaEncryption()
    end

    if g_game.getFeature(GameSequencedPackets) then
        scheduleEvent(function()
            self:enabledSequencedPackets()
        end, 1000)
    end

    self:recv()
end

function ProtocolLogin:onConnect()
    pdebug('[protocollogin] onConnect to ' .. tostring(self.host) .. ':' .. tostring(self.port))
    self.gotConnection = true
    self:connectCallback()
    self.connectCallback = nil
end

function ProtocolLogin:onRecv(msg)
    while not msg:eof() do
        local opcode = msg:getU8()
        pdebug('[protocollogin] recv opcode=' .. opcode)
        if opcode == LoginServerCaptchaChallenge then
            self:parseCaptchaChallenge(msg)
        elseif opcode == LoginServerCaptchaPassToken then
            self:parseCaptchaPassToken(msg)
        elseif opcode == LoginServerErrorNew then
            self:parseError(msg)
        elseif opcode == LoginServerError then
            self:parseError(msg)
        elseif opcode == LoginServerMotd then
            self:parseMotd(msg)
        elseif opcode == LoginServerUpdateNeeded then
            signalcall(self.onLoginError, self, tr('Client needs update.'))
        elseif opcode == LoginServerTokenSuccess then
            local unknown = msg:getU8()
        elseif opcode == LoginServerTokenError then
            -- TODO: prompt for token here
            local unknown = msg:getU8()
            signalcall(self.onLoginError, self, tr('Invalid authentification token.'))
        elseif opcode == LoginServerCharacterList then
            self:parseCharacterList(msg)
        elseif opcode == LoginServerExtendedCharacterList then
            self:parseExtendedCharacterList(msg)
        elseif opcode == LoginServerCastersList then
            self:parseCastersList(msg)
        elseif opcode == LoginServerUpdate then
            local signature = msg:getString()
            signalcall(self.onUpdateNeeded, self, signature)
        elseif opcode == LoginServerSessionKey then
            self:parseSessionKey(msg)
        else
            self:parseOpcode(opcode, msg)
        end
    end
    self:disconnect()
end

function ProtocolLogin:parseError(msg)
    local errorMessage = msg:getString()
    signalcall(self.onLoginError, self, errorMessage)
end

function ProtocolLogin:parseCaptchaChallenge(msg)
    local captchaId = msg:getU32()
    local imageBase64 = msg:getString()
    pdebug('[protocollogin] parseCaptchaChallenge id=' .. captchaId)
    signalcall(self.onCaptchaChallenge, self, captchaId, imageBase64)
end

function ProtocolLogin:parseCaptchaPassToken(msg)
    local passToken = msg:getString()
    signalcall(self.onCaptchaPassToken, self, passToken)
end

function ProtocolLogin:parseMotd(msg)
    local motd = msg:getString()
    signalcall(self.onMotd, self, motd)
end

function ProtocolLogin:parseSessionKey(msg)
    local sessionKey = msg:getString()
    signalcall(self.onSessionKey, self, sessionKey)
end

function ProtocolLogin:parseCharacterList(msg)
    local characters = {}

    if g_game.getClientVersion() > 1010 then
        local worlds = {}

        local worldsCount = msg:getU8()
        for i = 1, worldsCount do
            local world = {}
            local worldId = msg:getU8()
            world.worldName = msg:getString()
            world.worldIp = msg:getString()
            world.worldPort = msg:getU16()
            world.previewState = msg:getU8()
            worlds[worldId] = world
        end

        local charactersCount = msg:getU8()
        for i = 1, charactersCount do
            local character = {}
            local worldId = msg:getU8()
            character.name = msg:getString()
            character.worldName = worlds[worldId].worldName
            character.worldIp = worlds[worldId].worldIp
            character.worldPort = worlds[worldId].worldPort
            character.previewState = worlds[worldId].previewState
            characters[i] = character
        end

    else
        local charactersCount = msg:getU8()
        for i = 1, charactersCount do
            local character = {}
            character.name = msg:getString()
            character.worldName = msg:getString()
            character.worldIp = iptostring(msg:getU32())
            character.worldPort = msg:getU16()

            if g_game.getFeature(GamePreviewState) then
                character.previewState = msg:getU8()
            end

            -- Relencia: Extended character data for advanced character list
            if g_game.getFeature(GameEnterGameShowAppearance) then
                character.level = msg:getU16()
                character.vocation = msg:getString()
                character.outfitid = msg:getU16()
                character.headcolor = msg:getU8()
                character.torsocolor = msg:getU8()
                character.legscolor = msg:getU8()
                character.detailcolor = msg:getU8()
                character.addonsflags = msg:getU8()
                -- These aren't in our DB, use sensible defaults
                character.main = false
                character.dailyreward = 1  -- non-zero hides the "not collected" state
                character.hidden = false
            end

            characters[i] = character
        end
    end

    local account = {}
    if g_game.getProtocolVersion() > 1077 then
        account.status = msg:getU8()
        account.subStatus = msg:getU8()

        account.premDays = msg:getU32()
        if account.premDays ~= 0 and account.premDays ~= 65535 then
            account.premDays = math.floor((account.premDays - os.time()) / 86400)
        end
    else
        account.status = AccountStatus.Ok
        account.premDays = msg:getU16()
        account.subStatus = account.premDays > 0 and SubscriptionStatus.Premium or SubscriptionStatus.Free
    end

    signalcall(self.onCharacterList, self, characters, account)
end

function ProtocolLogin:parseExtendedCharacterList(msg)
    local characters = msg:getTable()
    local account = msg:getTable()
    local otui = msg:getString()
    signalcall(self.onCharacterList, self, characters, account, otui)
end

function ProtocolLogin:parseCastersList(msg)
    -- Game server connection info (same for all casters)
    local gameHost = iptostring(msg:getU32())
    local gamePort = msg:getU16()

    local casters = {}
    local count = msg:getU16()
    for i = 1, count do
        local caster = {}
        caster.id = msg:getU32()
        caster.name = msg:getString()
        caster.level = msg:getU16()
        caster.vocation = msg:getU8()
        caster.outfit = {
            type = msg:getU16(),
            head = msg:getU8(),
            body = msg:getU8(),
            legs = msg:getU8(),
            feet = msg:getU8(),
            addons = msg:getU16()
        }
        caster.castStartTime = msg:getU32()  -- Unix timestamp (seconds)
        caster.viewerCount = msg:getU16()
        casters[i] = caster
    end
    signalcall(self.onCastersList, self, casters, gameHost, gamePort)
end

function ProtocolLogin:parseOpcode(opcode, msg)
    signalcall(self.onOpcode, self, opcode, msg)
end

function ProtocolLogin:onError(msg, code)
    pdebug('[protocollogin] onError code=' .. tostring(code) .. ' msg=' .. tostring(msg))
    local text = translateNetworkError(code, self:isConnecting(), msg)
    signalcall(self.onLoginError, self, text)
end
