/*
 * Copyright 2019 Jeremy Jamet / Kunzisoft.
 *     
 * This file is part of KeePassDX.
 *
 *  KeePassDX 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 3 of the License, or
 *  (at your option) any later version.
 *
 *  KeePassDX 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 KeePassDX.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package com.kunzisoft.keepass.database.file.output

import android.util.Base64
import android.util.Log
import android.util.Xml
import com.kunzisoft.encrypt.StreamCipher
import com.kunzisoft.keepass.database.crypto.CrsAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
import com.kunzisoft.keepass.database.element.CustomData
import com.kunzisoft.keepass.database.element.CustomDataItem
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.DateInstant.Companion.toDotNetSeconds
import com.kunzisoft.keepass.database.element.DateInstant.Companion.toISO8601Format
import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.Tags
import com.kunzisoft.keepass.database.element.binary.BinaryData.Companion.BASE64_FLAG
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.element.entry.AutoType
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.node.NodeHandler
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41
import com.kunzisoft.keepass.database.file.DatabaseKDBXXML
import com.kunzisoft.keepass.stream.HashedBlockOutputStream
import com.kunzisoft.keepass.stream.HmacBlockOutputStream
import com.kunzisoft.keepass.utils.UnsignedInt
import com.kunzisoft.keepass.utils.longTo8Bytes
import com.kunzisoft.keepass.utils.readAllBytes
import com.kunzisoft.keepass.utils.uuidTo16Bytes
import com.kunzisoft.keepass.utils.write4BytesUInt
import com.kunzisoft.keepass.utils.writeByte
import org.xmlpull.v1.XmlSerializer
import java.io.IOException
import java.io.OutputStream
import java.security.SecureRandom
import java.util.Stack
import java.util.UUID
import java.util.zip.GZIPOutputStream
import javax.crypto.Cipher
import javax.crypto.CipherOutputStream
import kotlin.experimental.or


class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX)
    : DatabaseOutput<DatabaseHeaderKDBX>() {

    private var randomStream: StreamCipher? = null
    private lateinit var xml: XmlSerializer
    private var header: DatabaseHeaderKDBX? = null
    private var hashOfHeader: ByteArray? = null
    private var headerHmac: ByteArray? = null

    @Throws(DatabaseOutputException::class)
    override fun writeDatabase(outputStream: OutputStream,
                               assignMasterKey: () -> Unit) {

        try {
            header = outputHeader(outputStream, assignMasterKey)

            val osPlain: OutputStream = if (header!!.version.isBefore(FILE_VERSION_40)) {
                val cos = attachStreamEncryptor(header!!, outputStream)
                cos.write(header!!.streamStartBytes)

                HashedBlockOutputStream(cos)
            } else {
                outputStream.write(hashOfHeader!!)
                outputStream.write(headerHmac!!)

                attachStreamEncryptor(header!!, HmacBlockOutputStream(outputStream, mDatabaseKDBX.hmacKey!!))
            }

            when(mDatabaseKDBX.compressionAlgorithm) {
                CompressionAlgorithm.GZIP -> GZIPOutputStream(osPlain)
                else -> osPlain
            }.use { xmlOutputStream ->
                if (!header!!.version.isBefore(FILE_VERSION_40)) {
                    outputInnerHeader(mDatabaseKDBX, header!!, xmlOutputStream)
                }
                outputDatabase(xmlOutputStream)
            }
        } catch (e: Exception) {
            throw DatabaseOutputException(e)
        }
    }

    @Throws(IOException::class)
    private fun outputInnerHeader(database: DatabaseKDBX,
                                  header: DatabaseHeaderKDBX,
                                  dataOutputStream: OutputStream) {
        dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomStreamID)
        dataOutputStream.write4BytesUInt(UnsignedInt(4))
        if (header.innerRandomStream == null)
            throw IOException("Can't write innerRandomStream")
        dataOutputStream.write4BytesUInt(header.innerRandomStream!!.id)

        val streamKeySize = header.innerRandomStreamKey.size
        dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomstreamKey)
        dataOutputStream.write4BytesUInt(UnsignedInt(streamKeySize))
        dataOutputStream.write(header.innerRandomStreamKey)

        val binaryCache = database.binaryCache
        database.attachmentPool.doForEachOrderedBinaryWithoutDuplication { _, binary ->
            // Force decompression to add binary in header
            binary.decompress(binaryCache)
            // Write type binary
            dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary)
            // Write size
            dataOutputStream.write4BytesUInt(UnsignedInt.fromKotlinLong(binary.getSize() + 1))
            // Write protected flag
            var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
            if (binary.isProtected) {
                flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
            }
            dataOutputStream.writeByte(flag)

            binary.getInputDataStream(binaryCache).use { inputStream ->
                inputStream.readAllBytes { buffer ->
                    dataOutputStream.write(buffer)
                }
            }
        }

        dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader)
        dataOutputStream.write4BytesUInt(UnsignedInt(0))
    }

    @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
    private fun outputDatabase(outputStream: OutputStream) {

        xml = Xml.newSerializer()

        xml.setOutput(outputStream, "UTF-8")
        xml.startDocument("UTF-8", true)

        xml.startTag(null, DatabaseKDBXXML.ElemDocNode)

        writeMeta()

        mDatabaseKDBX.rootGroup?.let { root ->
            xml.startTag(null, DatabaseKDBXXML.ElemRoot)
            startGroup(root)
            val groupStack = Stack<GroupKDBX>()
            groupStack.push(root)

            if (!root.doForEachChild(
                            object : NodeHandler<EntryKDBX>() {
                                override fun operate(node: EntryKDBX): Boolean {
                                    try {
                                        writeEntry(node, false)
                                    } catch (ex: IOException) {
                                        throw RuntimeException(ex)
                                    }

                                    return true
                                }
                            },
                            object : NodeHandler<GroupKDBX>() {
                                override fun operate(node: GroupKDBX): Boolean {
                                    while (true) {
                                        try {
                                            if (node.parent === groupStack.peek()) {
                                                groupStack.push(node)
                                                startGroup(node)
                                                break
                                            } else {
                                                groupStack.pop()
                                                if (groupStack.size <= 0) return false
                                                endGroup()
                                            }
                                        } catch (e: IOException) {
                                            throw RuntimeException(e)
                                        }

                                    }
                                    return true
                                }
                            })
            )
                throw RuntimeException("Writing groups failed")

            while (groupStack.size > 1) {
                xml.endTag(null, DatabaseKDBXXML.ElemGroup)
                groupStack.pop()
            }
        }

        endGroup()

        writeDeletedObjects(mDatabaseKDBX.deletedObjects)

        xml.endTag(null, DatabaseKDBXXML.ElemRoot)

        xml.endTag(null, DatabaseKDBXXML.ElemDocNode)
        xml.endDocument()
    }

    @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
    private fun writeMeta() {
        xml.startTag(null, DatabaseKDBXXML.ElemMeta)

        writeString(DatabaseKDBXXML.ElemGenerator, mDatabaseKDBX.localizedAppName)

        if (hashOfHeader != null) {
            writeString(DatabaseKDBXXML.ElemHeaderHash, String(Base64.encode(hashOfHeader!!, BASE64_FLAG)))
        }

        if (!header!!.version.isBefore(FILE_VERSION_40)) {
            writeDateInstant(DatabaseKDBXXML.ElemSettingsChanged, mDatabaseKDBX.settingsChanged)
        }
        writeString(DatabaseKDBXXML.ElemDbName, mDatabaseKDBX.name, true)
        writeDateInstant(DatabaseKDBXXML.ElemDbNameChanged, mDatabaseKDBX.nameChanged)
        writeString(DatabaseKDBXXML.ElemDbDesc, mDatabaseKDBX.description, true)
        writeDateInstant(DatabaseKDBXXML.ElemDbDescChanged, mDatabaseKDBX.descriptionChanged)
        writeString(DatabaseKDBXXML.ElemDbDefaultUser, mDatabaseKDBX.defaultUserName, true)
        writeDateInstant(DatabaseKDBXXML.ElemDbDefaultUserChanged, mDatabaseKDBX.defaultUserNameChanged)
        writeLong(DatabaseKDBXXML.ElemDbMntncHistoryDays, mDatabaseKDBX.maintenanceHistoryDays.toKotlinLong())
        writeString(DatabaseKDBXXML.ElemDbColor, mDatabaseKDBX.color)
        writeDateInstant(DatabaseKDBXXML.ElemDbKeyChanged, mDatabaseKDBX.keyLastChanged)
        writeLong(DatabaseKDBXXML.ElemDbKeyChangeRec, mDatabaseKDBX.keyChangeRecDays)
        writeLong(DatabaseKDBXXML.ElemDbKeyChangeForce, mDatabaseKDBX.keyChangeForceDays)

        writeMemoryProtection(mDatabaseKDBX.memoryProtection)

        writeCustomIconList()

        writeBoolean(DatabaseKDBXXML.ElemRecycleBinEnabled, mDatabaseKDBX.isRecycleBinEnabled)
        writeUuid(DatabaseKDBXXML.ElemRecycleBinUuid, mDatabaseKDBX.recycleBinUUID)
        writeDateInstant(DatabaseKDBXXML.ElemRecycleBinChanged, mDatabaseKDBX.recycleBinChanged)
        writeUuid(DatabaseKDBXXML.ElemEntryTemplatesGroup, mDatabaseKDBX.entryTemplatesGroup)
        writeDateInstant(DatabaseKDBXXML.ElemEntryTemplatesGroupChanged, mDatabaseKDBX.entryTemplatesGroupChanged)
        writeLong(DatabaseKDBXXML.ElemHistoryMaxItems, mDatabaseKDBX.historyMaxItems.toLong())
        writeLong(DatabaseKDBXXML.ElemHistoryMaxSize, mDatabaseKDBX.historyMaxSize)
        writeUuid(DatabaseKDBXXML.ElemLastSelectedGroup, mDatabaseKDBX.lastSelectedGroupUUID)
        writeUuid(DatabaseKDBXXML.ElemLastTopVisibleGroup, mDatabaseKDBX.lastTopVisibleGroupUUID)

        // Seem to work properly if always in meta
        if (header!!.version.isBefore(FILE_VERSION_40))
            writeMetaBinaries()

        writeCustomData(mDatabaseKDBX.customData)

        xml.endTag(null, DatabaseKDBXXML.ElemMeta)
    }

    @Throws(DatabaseOutputException::class)
    private fun attachStreamEncryptor(header: DatabaseHeaderKDBX, os: OutputStream): CipherOutputStream {
        val cipher: Cipher
        try {
            cipher = mDatabaseKDBX
                .encryptionAlgorithm
                .cipherEngine
                .getCipher(Cipher.ENCRYPT_MODE, mDatabaseKDBX.finalKey!!, header.encryptionIV)
        } catch (e: Exception) {
            throw DatabaseOutputException("Invalid algorithm.", e)
        }

        return CipherOutputStream(os, cipher)
    }

    @Throws(DatabaseOutputException::class)
    override fun setIVs(header: DatabaseHeaderKDBX): SecureRandom {
        val random = super.setIVs(header)
        random.nextBytes(header.masterSeed)

        val ivLength = mDatabaseKDBX.encryptionAlgorithm.cipherEngine.ivLength()
        if (ivLength != header.encryptionIV.size) {
            header.encryptionIV = ByteArray(ivLength)
        }
        random.nextBytes(header.encryptionIV)

        if (mDatabaseKDBX.kdfParameters == null) {
            mDatabaseKDBX.kdfParameters = KdfFactory.aesKdf.defaultParameters
        }

        mDatabaseKDBX.randomizeKdfParameters()

        if (header.version.isBefore(FILE_VERSION_40)) {
            header.innerRandomStream = CrsAlgorithm.Salsa20
            header.innerRandomStreamKey = ByteArray(32)
        } else {
            header.innerRandomStream = CrsAlgorithm.ChaCha20
            header.innerRandomStreamKey = ByteArray(64)
        }
        random.nextBytes(header.innerRandomStreamKey)

        try {
            randomStream = CrsAlgorithm.getCipher(header.innerRandomStream, header.innerRandomStreamKey)
        } catch (e: Exception) {
            throw DatabaseOutputException(e)
        }

        if (header.version.isBefore(FILE_VERSION_40)) {
            random.nextBytes(header.streamStartBytes)
        }

        return random
    }

    @Throws(DatabaseOutputException::class)
    private fun outputHeader(outputStream: OutputStream,
                             assignMasterKey: () -> Unit): DatabaseHeaderKDBX {
        try {
            val header = DatabaseHeaderKDBX(mDatabaseKDBX)
            setIVs(header)

            mDatabaseKDBX.transformSeed = header.transformSeed
            assignMasterKey.invoke()

            val pho = DatabaseHeaderOutputKDBX(mDatabaseKDBX, header, outputStream)
            pho.output()

            hashOfHeader = pho.hashOfHeader
            headerHmac = pho.headerHmac

            return header
        } catch (e: IOException) {
            throw DatabaseOutputException("Failed to output the header.", e)
        }
    }

    @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
    private fun startGroup(group: GroupKDBX) {
        xml.startTag(null, DatabaseKDBXXML.ElemGroup)
        writeUuid(DatabaseKDBXXML.ElemUuid, group.id)
        writeString(DatabaseKDBXXML.ElemName, group.title)
        writeString(DatabaseKDBXXML.ElemNotes, group.notes)
        writeLong(DatabaseKDBXXML.ElemIcon, group.icon.standard.id.toLong())

        if (!group.icon.custom.isUnknown) {
            writeUuid(DatabaseKDBXXML.ElemCustomIconID, group.icon.custom.uuid)
        }

        writeTags(group.tags)
        writePreviousParentGroup(group.previousParentGroup)
        writeTimes(group)
        writeBoolean(DatabaseKDBXXML.ElemIsExpanded, group.isExpanded)
        writeString(DatabaseKDBXXML.ElemGroupDefaultAutoTypeSeq, group.defaultAutoTypeSequence)
        writeBoolean(DatabaseKDBXXML.ElemEnableAutoType, group.enableAutoType)
        writeBoolean(DatabaseKDBXXML.ElemEnableSearching, group.enableSearching)
        writeUuid(DatabaseKDBXXML.ElemLastTopVisibleEntry, group.lastTopVisibleEntry)
        writeCustomData(group.customData)
    }

    @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
    private fun endGroup() {
        xml.endTag(null, DatabaseKDBXXML.ElemGroup)
    }

    @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
    private fun writeEntry(entry: EntryKDBX, isHistory: Boolean) {

        xml.startTag(null, DatabaseKDBXXML.ElemEntry)

        writeUuid(DatabaseKDBXXML.ElemUuid, entry.id)
        writeLong(DatabaseKDBXXML.ElemIcon, entry.icon.standard.id.toLong())

        if (!entry.icon.custom.isUnknown) {
            writeUuid(DatabaseKDBXXML.ElemCustomIconID, entry.icon.custom.uuid)
        }

        writeString(DatabaseKDBXXML.ElemFgColor, entry.foregroundColor)
        writeString(DatabaseKDBXXML.ElemBgColor, entry.backgroundColor)
        writeString(DatabaseKDBXXML.ElemOverrideUrl, entry.overrideURL)

        // Write quality check only if false
        if (!entry.qualityCheck) {
            writeBoolean(DatabaseKDBXXML.ElemQualityCheck, entry.qualityCheck)
        }
        writeTags(entry.tags)
        writePreviousParentGroup(entry.previousParentGroup)
        writeTimes(entry)
        writeFields(entry.getFields())
        writeEntryBinaries(entry.binaries)
        writeCustomData(entry.customData)
        writeAutoType(entry.autoType)

        if (!isHistory) {
            writeEntryHistory(entry.history)
        }

        xml.endTag(null, DatabaseKDBXXML.ElemEntry)
    }

    @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
    private fun writeString(name: String, value: String, filterXmlChars: Boolean = false) {
        var xmlString = value

        xml.startTag(null, name)

        if (filterXmlChars) {
            xmlString = safeXmlString(xmlString)
        }

        xml.text(xmlString)
        xml.endTag(null, name)
    }

    @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
    private fun writeDateInstant(name: String, date: DateInstant) {
        if (header!!.version.isBefore(FILE_VERSION_40)) {
            writeString(name, date.toISO8601Format())
        } else {
            writeString(name, String(
                Base64.encode(
                    longTo8Bytes(date.toDotNetSeconds()), BASE64_FLAG)
                )
            )
        }
    }

    @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
    private fun writeLong(name: String, value: Long) {
        writeString(name, value.toString())
    }

    @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
    private fun writeBoolean(name: String, value: Boolean?) {
        val text: String = when {
            value == null -> DatabaseKDBXXML.ValNull
            value -> DatabaseKDBXXML.ValTrue
            else -> DatabaseKDBXXML.ValFalse
        }

        writeString(name, text)
    }

    @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
    private fun writeUuid(name: String, uuid: UUID) {
        val data = uuidTo16Bytes(uuid)
        writeString(name, String(Base64.encode(data, BASE64_FLAG)))
    }

    /*
    // Normally used by a single entry but obsolete because binaries are in meta tag with kdbx3.1-
    // or in file header with kdbx4
    // binary.isProtected attribute is not used to create the XML
    @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
    private fun writeEntryBinary(binary : BinaryAttachment) {
        if (binary.length() > 0) {
            if (binary.isProtected) {
                xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
                binary.getInputDataStream().use { inputStream ->
                    inputStream.readBytes { buffer ->
                        val encoded = ByteArray(buffer.size)
                        randomStream!!.processBytes(buffer, 0, encoded.size, encoded, 0)
                        xml.text(String(Base64.encode(encoded, BASE_64_FLAG)))
                    }
                }
            } else {
                // Write the XML
                binary.getInputDataStream().use { inputStream ->
                    inputStream.readBytes { buffer ->
                        xml.text(String(Base64.encode(buffer, BASE_64_FLAG)))
                    }
                }
            }
        }
    }
    */

    // Only uses with kdbx3.1 to write binaries in meta tag
    // With kdbx4, don't use this method because binaries are in header file
    @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
    private fun writeMetaBinaries() {
        xml.startTag(null, DatabaseKDBXXML.ElemBinaries)
        // Use indexes because necessarily (binary header ref is the order)
        val binaryCache = mDatabaseKDBX.binaryCache
        mDatabaseKDBX.attachmentPool.doForEachOrderedBinaryWithoutDuplication { index, binary ->
            xml.startTag(null, DatabaseKDBXXML.ElemBinary)
            xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString())
            if (binary.getSize() > 0) {
                if (binary.isCompressed) {
                    xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue)
                }
                try {
                    // Write the XML
                    binary.getInputDataStream(binaryCache).use { inputStream ->
                        inputStream.readAllBytes { buffer ->
                            xml.text(String(Base64.encode(buffer, BASE64_FLAG)))
                        }
                    }
                } catch (e: Exception) {
                    Log.e(TAG, "Unable to write binary", e)
                }
            }
            xml.endTag(null, DatabaseKDBXXML.ElemBinary)
        }
        xml.endTag(null, DatabaseKDBXXML.ElemBinaries)
    }

    @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
    private fun writeAutoType(autoType: AutoType) {
        xml.startTag(null, DatabaseKDBXXML.ElemAutoType)

        writeBoolean(DatabaseKDBXXML.ElemAutoTypeEnabled, autoType.enabled)
        writeLong(DatabaseKDBXXML.ElemAutoTypeObfuscation, autoType.obfuscationOptions.toKotlinLong())

        if (autoType.defaultSequence.isNotEmpty()) {
            writeString(DatabaseKDBXXML.ElemAutoTypeDefaultSeq, autoType.defaultSequence, true)
        }

        autoType.doForEachAutoTypeItem { key, value ->
            xml.startTag(null, DatabaseKDBXXML.ElemAutoTypeItem)

            xml.startTag(null, DatabaseKDBXXML.ElemWindow)
            xml.text(safeXmlString(key))
            xml.endTag(null, DatabaseKDBXXML.ElemWindow)

            xml.startTag(null, DatabaseKDBXXML.ElemKeystrokeSequence)
            xml.text(safeXmlString(value))
            xml.endTag(null, DatabaseKDBXXML.ElemKeystrokeSequence)

            xml.endTag(null, DatabaseKDBXXML.ElemAutoTypeItem)
        }

        xml.endTag(null, DatabaseKDBXXML.ElemAutoType)
    }

    @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
    private fun writeFields(fields: List<Field>) {
        for (field in fields) {
            writeField(field)
        }
    }

    @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
    private fun writeField(field: Field) {
        val label = field.name
        val value = field.protectedValue

        xml.startTag(null, DatabaseKDBXXML.ElemString)
        xml.startTag(null, DatabaseKDBXXML.ElemKey)
        xml.text(safeXmlString(label))
        xml.endTag(null, DatabaseKDBXXML.ElemKey)

        xml.startTag(null, DatabaseKDBXXML.ElemValue)
        var protect = value.isProtected

        when (label) {
            MemoryProtectionConfig.TITLE_FIELD -> protect = mDatabaseKDBX.memoryProtection.protectTitle
            MemoryProtectionConfig.USERNAME_FIELD -> protect = mDatabaseKDBX.memoryProtection.protectUserName
            MemoryProtectionConfig.PASSWORD_FIELD -> protect = mDatabaseKDBX.memoryProtection.protectPassword
            MemoryProtectionConfig.URL_FIELD -> protect = mDatabaseKDBX.memoryProtection.protectUrl
            MemoryProtectionConfig.NOTES_FIELD -> protect = mDatabaseKDBX.memoryProtection.protectNotes
        }

        if (protect) {
            xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
            val data = value.toString().toByteArray()
            val encoded = randomStream?.processBytes(data) ?: ByteArray(0)
            xml.text(String(Base64.encode(encoded, BASE64_FLAG)))
        } else {
            xml.text(value.toString())
        }

        xml.endTag(null, DatabaseKDBXXML.ElemValue)
        xml.endTag(null, DatabaseKDBXXML.ElemString)
    }

    @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
    private fun writeDeletedObject(value: DeletedObject) {
        xml.startTag(null, DatabaseKDBXXML.ElemDeletedObject)

        writeUuid(DatabaseKDBXXML.ElemUuid, value.uuid)
        writeDateInstant(DatabaseKDBXXML.ElemDeletionTime, value.deletionTime)

        xml.endTag(null, DatabaseKDBXXML.ElemDeletedObject)
    }

    @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
    private fun writeEntryBinaries(binaries: LinkedHashMap<String, Int>) {
        for ((label, poolId) in binaries) {
            // Retrieve the right index with the poolId, don't use ref because of header in DatabaseV4
            mDatabaseKDBX.attachmentPool.getBinaryIndexFromKey(poolId)?.toString()?.let { indexString ->
                xml.startTag(null, DatabaseKDBXXML.ElemBinary)
                xml.startTag(null, DatabaseKDBXXML.ElemKey)
                xml.text(safeXmlString(label))
                xml.endTag(null, DatabaseKDBXXML.ElemKey)

                xml.startTag(null, DatabaseKDBXXML.ElemValue)
                // Use only pool data in Meta to save binaries
                xml.attribute(null, DatabaseKDBXXML.AttrRef, indexString)
                xml.endTag(null, DatabaseKDBXXML.ElemValue)

                xml.endTag(null, DatabaseKDBXXML.ElemBinary)
            }
        }
    }

    @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
    private fun writeDeletedObjects(value: Collection<DeletedObject>) {
        xml.startTag(null, DatabaseKDBXXML.ElemDeletedObjects)

        for (pdo in value) {
            writeDeletedObject(pdo)
        }

        xml.endTag(null, DatabaseKDBXXML.ElemDeletedObjects)
    }

    @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
    private fun writeMemoryProtection(value: MemoryProtectionConfig) {
        xml.startTag(null, DatabaseKDBXXML.ElemMemoryProt)

        writeBoolean(DatabaseKDBXXML.ElemProtTitle, value.protectTitle)
        writeBoolean(DatabaseKDBXXML.ElemProtUserName, value.protectUserName)
        writeBoolean(DatabaseKDBXXML.ElemProtPassword, value.protectPassword)
        writeBoolean(DatabaseKDBXXML.ElemProtURL, value.protectUrl)
        writeBoolean(DatabaseKDBXXML.ElemProtNotes, value.protectNotes)

        xml.endTag(null, DatabaseKDBXXML.ElemMemoryProt)
    }

    @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
    private fun writeCustomData(customData: CustomData) {
        if (customData.isNotEmpty()) {
            xml.startTag(null, DatabaseKDBXXML.ElemCustomData)

            customData.doForEachItems { customDataItem ->
                writeCustomDataItem(customDataItem)
            }

            xml.endTag(null, DatabaseKDBXXML.ElemCustomData)
        }
    }

    private fun writeCustomDataItem(customDataItem: CustomDataItem) {
        xml.startTag(null, DatabaseKDBXXML.ElemStringDictExItem)

        xml.startTag(null, DatabaseKDBXXML.ElemKey)
        xml.text(safeXmlString(customDataItem.key))
        xml.endTag(null, DatabaseKDBXXML.ElemKey)

        xml.startTag(null, DatabaseKDBXXML.ElemValue)
        xml.text(safeXmlString(customDataItem.value))
        xml.endTag(null, DatabaseKDBXXML.ElemValue)

        customDataItem.lastModificationTime?.let { lastModificationTime ->
            writeDateInstant(DatabaseKDBXXML.ElemLastModTime, lastModificationTime)
        }

        xml.endTag(null, DatabaseKDBXXML.ElemStringDictExItem)
    }

    @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
    private fun writeTags(tags: Tags) {
        if (tags.isNotEmpty()) {
            writeString(DatabaseKDBXXML.ElemTags, tags.toString())
        }
    }

    @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
    private fun writePreviousParentGroup(previousParentGroup: UUID) {
        if (!header!!.version.isBefore(FILE_VERSION_41)
                && previousParentGroup != DatabaseVersioned.UUID_ZERO) {
            writeUuid(DatabaseKDBXXML.ElemPreviousParentGroup, previousParentGroup)
        }
    }

    @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
    private fun writeTimes(node: NodeKDBXInterface) {
        xml.startTag(null, DatabaseKDBXXML.ElemTimes)

        writeDateInstant(DatabaseKDBXXML.ElemLastModTime, node.lastModificationTime)
        writeDateInstant(DatabaseKDBXXML.ElemCreationTime, node.creationTime)
        writeDateInstant(DatabaseKDBXXML.ElemLastAccessTime, node.lastAccessTime)
        writeDateInstant(DatabaseKDBXXML.ElemExpiryTime, node.expiryTime)
        writeBoolean(DatabaseKDBXXML.ElemExpires, node.expires)
        writeLong(DatabaseKDBXXML.ElemUsageCount, node.usageCount.toKotlinLong())
        writeDateInstant(DatabaseKDBXXML.ElemLocationChanged, node.locationChanged)

        xml.endTag(null, DatabaseKDBXXML.ElemTimes)
    }

    @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
    private fun writeEntryHistory(value: List<EntryKDBX>) {
        val element = DatabaseKDBXXML.ElemHistory

        xml.startTag(null, element)

        for (entry in value) {
            writeEntry(entry, true)
        }

        xml.endTag(null, element)
    }

    @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
    private fun writeCustomIconList() {
        var firstElement = true
        val binaryCache = mDatabaseKDBX.binaryCache
        mDatabaseKDBX.iconsManager.doForEachCustomIcon { iconCustom, binary ->
            if (binary.dataExists()) {
                // Write the parent tag
                if (firstElement) {
                    xml.startTag(null, DatabaseKDBXXML.ElemCustomIcons)
                    firstElement = false
                }

                xml.startTag(null, DatabaseKDBXXML.ElemCustomIconItem)

                writeUuid(DatabaseKDBXXML.ElemCustomIconItemID, iconCustom.uuid)
                var customImageData = ByteArray(0)
                try {
                    binary.getInputDataStream(binaryCache).use { inputStream ->
                        customImageData = inputStream.readBytes()
                    }
                } catch (e: Exception) {
                    Log.e(TAG, "Unable to write custom icon", e)
                } finally {
                    writeString(DatabaseKDBXXML.ElemCustomIconItemData,
                            String(Base64.encode(customImageData, BASE64_FLAG)))
                }
                if (iconCustom.name.isNotEmpty()) {
                    writeString(DatabaseKDBXXML.ElemName, iconCustom.name)
                }
                iconCustom.lastModificationTime?.let { lastModificationTime ->
                    writeDateInstant(DatabaseKDBXXML.ElemLastModTime, lastModificationTime)
                }

                xml.endTag(null, DatabaseKDBXXML.ElemCustomIconItem)
            }
        }
        // Close the parent tag
        if (!firstElement) {
            xml.endTag(null, DatabaseKDBXXML.ElemCustomIcons)
        }
    }

    private fun safeXmlString(text: String): String {
        if (text.isEmpty()) {
            return text
        }
        val stringBuilder = StringBuilder()
        var character: Char
        for (element in text) {
            character = element
            val hexChar = character.code
            if (
                    hexChar in 0x20..0xD7FF ||
                    hexChar == 0x9 ||
                    hexChar == 0xA ||
                    hexChar == 0xD ||
                    hexChar in 0xE000..0xFFFD
            ) {
                stringBuilder.append(character)
            }
        }
        return stringBuilder.toString()
    }

    companion object {
        private val TAG = DatabaseOutputKDBX::class.java.name
    }
}
