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

LoginServerError = 10
LoginServerTokenSuccess = 12
LoginServerTokenError = 13
LoginServerUpdate = 17
LoginServerMotd = 20
LoginServerUpdateNeeded = 30
LoginServerSessionKey = 40
LoginServerCharacterList = 100
LoginServerExtendedCharacterList = 101

-- Since 10.76
LoginServerRetry = 10
LoginServerErrorNew = 11

function ProtocolLogin:login(host, port, accountName, accountPassword, authenticatorToken, stayLogged)
    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.accountName = accountName
    self.accountPassword = accountPassword
    self.authenticatorToken = authenticatorToken
    self.stayLogged = stayLogged
    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)

    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()
    self.gotConnection = true
    self:connectCallback()
    self.connectCallback = nil
end

function ProtocolLogin:onRecv(msg)
    while not msg:eof() do
        local opcode = msg:getU8()
        if 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 == 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: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

            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:parseOpcode(opcode, msg)
    signalcall(self.onOpcode, self, opcode, msg)
end

function ProtocolLogin:onError(msg, code)
    local text = translateNetworkError(code, self:isConnecting(), msg)
    signalcall(self.onLoginError, self, text)
end
