package com.meituan.android.walle;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;


public final class PayloadWriter {
    private PayloadWriter() {
        super();
    }

    /**
     * put (id, String) into apk, update if id exists
     * @param apkFile apk file
     * @param id id
     * @param string string content
     * @throws IOException
     * @throws SignatureNotFoundException
     */
    public static void put(final File apkFile, final int id, final String string) throws IOException, SignatureNotFoundException {
        put(apkFile, id, string, false);
    }
    /**
     * put (id, String) into apk, update if id exists
     * @param apkFile apk file
     * @param id id
     * @param string string
     * @param lowMemory if need low memory operation, maybe a little slower
     * @throws IOException
     * @throws SignatureNotFoundException
     */
    public static void put(final File apkFile, final int id, final String string, final boolean lowMemory) throws IOException, SignatureNotFoundException {
        final byte[] bytes = string.getBytes(ApkUtil.DEFAULT_CHARSET);
        final ByteBuffer byteBuffer = ByteBuffer.allocate(bytes.length);
        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
        byteBuffer.put(bytes, 0, bytes.length);
        byteBuffer.flip();
        put(apkFile, id, byteBuffer, lowMemory);
    }
    /**
     * put (id, buffer) into apk, update if id exists
     *
     * @param apkFile apk file
     * @param id      id
     * @param buffer  buffer
     * @throws IOException
     * @throws SignatureNotFoundException
     */
    public static void put(final File apkFile, final int id, final ByteBuffer buffer) throws IOException, SignatureNotFoundException {
        put(apkFile, id, buffer, false);
    }

    /**
     * put (id, buffer) into apk, update if id exists
     * @param apkFile apk file
     * @param id id
     * @param buffer buffer
     * @param lowMemory if need low memory operation, maybe a little slower
     * @throws IOException
     * @throws SignatureNotFoundException
     */
    public static void put(final File apkFile, final int id, final ByteBuffer buffer, final boolean lowMemory) throws IOException, SignatureNotFoundException {
        final Map<Integer, ByteBuffer> idValues = new HashMap<Integer, ByteBuffer>();
        idValues.put(id, buffer);
        putAll(apkFile, idValues, lowMemory);
    }
    /**
     * put new idValues into apk, update if id exists
     *
     * @param apkFile  apk file
     * @param idValues id value. NOTE: use unknown IDs. DO NOT use ID that have already been used.  See <a href='https://source.android.com/security/apksigning/v2.html'>APK Signature Scheme v2</a>
     * @throws IOException
     * @throws SignatureNotFoundException
     */
    public static void putAll(final File apkFile, final Map<Integer, ByteBuffer> idValues) throws IOException, SignatureNotFoundException {
        putAll(apkFile, idValues, false);
    }
    /**
     * put new idValues into apk, update if id exists
     *
     * @param apkFile  apk file
     * @param idValues id value. NOTE: use unknown IDs. DO NOT use ID that have already been used.  See <a href='https://source.android.com/security/apksigning/v2.html'>APK Signature Scheme v2</a>
     * @param lowMemory if need low memory operation, maybe a little slower
     * @throws IOException
     * @throws SignatureNotFoundException
     */
    public static void putAll(final File apkFile, final Map<Integer, ByteBuffer> idValues, final boolean lowMemory) throws IOException, SignatureNotFoundException {
        handleApkSigningBlock(apkFile, new ApkSigningBlockHandler() {
            @Override
            public ApkSigningBlock handle(final Map<Integer, ByteBuffer> originIdValues) {
                if (idValues != null && !idValues.isEmpty()) {
                    originIdValues.putAll(idValues);
                }
                final ApkSigningBlock apkSigningBlock = new ApkSigningBlock();
                final Set<Map.Entry<Integer, ByteBuffer>> entrySet = originIdValues.entrySet();
                for (Map.Entry<Integer, ByteBuffer> entry : entrySet) {
                    final ApkSigningPayload payload = new ApkSigningPayload(entry.getKey(), entry.getValue());
                    apkSigningBlock.addPayload(payload);
                }
                return apkSigningBlock;
            }
        }, lowMemory);
    }
    /**
     * remove content by id
     *
     * @param apkFile apk file
     * @param id id
     * @throws IOException
     * @throws SignatureNotFoundException
     */
    public static void remove(final File apkFile, final int id) throws IOException, SignatureNotFoundException {
        remove(apkFile, id, false);
    }
    /**
     * remove content by id
     *
     * @param apkFile apk file
     * @param id id
     * @param lowMemory  if need low memory operation, maybe a little slower
     * @throws IOException
     * @throws SignatureNotFoundException
     */
    public static void remove(final File apkFile, final int id, final boolean lowMemory) throws IOException, SignatureNotFoundException {
        PayloadWriter.handleApkSigningBlock(apkFile, new PayloadWriter.ApkSigningBlockHandler() {
            @Override
            public ApkSigningBlock handle(final Map<Integer, ByteBuffer> originIdValues) {
                final ApkSigningBlock apkSigningBlock = new ApkSigningBlock();
                final Set<Map.Entry<Integer, ByteBuffer>> entrySet = originIdValues.entrySet();
                for (Map.Entry<Integer, ByteBuffer> entry : entrySet) {
                    if (entry.getKey() != id) {
                        final ApkSigningPayload payload = new ApkSigningPayload(entry.getKey(), entry.getValue());
                        apkSigningBlock.addPayload(payload);
                    }
                }
                return apkSigningBlock;
            }
        }, lowMemory);
    }

    interface ApkSigningBlockHandler {
        ApkSigningBlock handle(Map<Integer, ByteBuffer> originIdValues);
    }

    static void handleApkSigningBlock(final File apkFile, final ApkSigningBlockHandler handler, final boolean lowMemory) throws IOException, SignatureNotFoundException {
        RandomAccessFile fIn = null;
        FileChannel fileChannel = null;
        try {
            fIn = new RandomAccessFile(apkFile, "rw");
            fileChannel = fIn.getChannel();
            final long commentLength = ApkUtil.getCommentLength(fileChannel);
            final long centralDirStartOffset = ApkUtil.findCentralDirStartOffset(fileChannel, commentLength);
            // Find the APK Signing Block. The block immediately precedes the Central Directory.
            final Pair<ByteBuffer, Long> apkSigningBlockAndOffset = ApkUtil.findApkSigningBlock(fileChannel, centralDirStartOffset);
            final ByteBuffer apkSigningBlock2 = apkSigningBlockAndOffset.getFirst();
            final long apkSigningBlockOffset = apkSigningBlockAndOffset.getSecond();

            final Map<Integer, ByteBuffer> originIdValues = ApkUtil.findIdValues(apkSigningBlock2);
            // Find the APK Signature Scheme v2 Block inside the APK Signing Block.
            final ByteBuffer apkSignatureSchemeV2Block = originIdValues.get(ApkUtil.APK_SIGNATURE_SCHEME_V2_BLOCK_ID);

            if (apkSignatureSchemeV2Block == null) {
                throw new IOException(
                        "No APK Signature Scheme v2 block in APK Signing Block");
            }

            final boolean needPadding = originIdValues.remove(ApkUtil.VERITY_PADDING_BLOCK_ID) != null;
            final ApkSigningBlock apkSigningBlock = handler.handle(originIdValues);
            // replace VERITY_PADDING_BLOCK with new one
            if (needPadding) {
                // uint64:  size (excluding this field)
                // repeated ID-value pairs:
                //     uint64:           size (excluding this field)
                //     uint32:           ID
                //     (size - 4) bytes: value
                // (extra dummy ID-value for padding to make block size a multiple of 4096 bytes)
                // uint64:  size (same as the one above)
                // uint128: magic

                int blocksSize = 0;
                for (ApkSigningPayload payload : apkSigningBlock.getPayloads()) {
                    blocksSize += payload.getTotalSize();
                }

                int resultSize = 8 + blocksSize + 8 + 16; // size(uint64) + pairs size + size(uint64) + magic(uint128)
                if (resultSize % ApkUtil.ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0) {
                    int padding = ApkUtil.ANDROID_COMMON_PAGE_ALIGNMENT_BYTES - 12 // size(uint64) + id(uint32)
                            - (resultSize % ApkUtil.ANDROID_COMMON_PAGE_ALIGNMENT_BYTES);
                    if (padding < 0) {
                        padding += ApkUtil.ANDROID_COMMON_PAGE_ALIGNMENT_BYTES;
                    }
                    final ByteBuffer dummy =  ByteBuffer.allocate(padding).order(ByteOrder.LITTLE_ENDIAN);
                    apkSigningBlock.addPayload(new ApkSigningPayload(ApkUtil.VERITY_PADDING_BLOCK_ID,dummy));
                }
            }

            if (apkSigningBlockOffset != 0 && centralDirStartOffset != 0) {

                // read CentralDir
                fIn.seek(centralDirStartOffset);

                byte[] centralDirBytes = null;
                File tempCentralBytesFile = null;
                // read CentralDir
                if (lowMemory) {
                    tempCentralBytesFile = new File(apkFile.getParent(), UUID.randomUUID().toString());
                    FileOutputStream outStream = null;
                    try {
                        outStream = new FileOutputStream(tempCentralBytesFile);
                        final byte[] buffer = new byte[1024];

                        int len;
                        while ((len = fIn.read(buffer)) > 0){
                            outStream.write(buffer, 0, len);
                        }
                    } finally {
                        if (outStream != null) {
                            outStream.close();
                        }
                    }
                } else {
                    centralDirBytes = new byte[(int) (fileChannel.size() - centralDirStartOffset)];
                    fIn.read(centralDirBytes);
                }

                //update apk sign
                fileChannel.position(apkSigningBlockOffset);
                final long length = apkSigningBlock.writeApkSigningBlock(fIn);

                // update CentralDir
                if (lowMemory) {
                    FileInputStream inputStream = null;
                    try {
                        inputStream = new FileInputStream(tempCentralBytesFile);
                        final byte[] buffer = new byte[1024];

                        int len;
                        while ((len = inputStream.read(buffer)) > 0){
                            fIn.write(buffer, 0, len);
                        }
                    } finally {
                        if (inputStream != null) {
                            inputStream.close();
                        }
                        tempCentralBytesFile.delete();
                    }
                } else {
                    // store CentralDir
                    fIn.write(centralDirBytes);
                }
                // update length
                fIn.setLength(fIn.getFilePointer());

                // update CentralDir Offset

                // End of central directory record (EOCD)
                // Offset     Bytes     Description[23]
                // 0            4       End of central directory signature = 0x06054b50
                // 4            2       Number of this disk
                // 6            2       Disk where central directory starts
                // 8            2       Number of central directory records on this disk
                // 10           2       Total number of central directory records
                // 12           4       Size of central directory (bytes)
                // 16           4       Offset of start of central directory, relative to start of archive
                // 20           2       Comment length (n)
                // 22           n       Comment

                fIn.seek(fileChannel.size() - commentLength - 6);
                // 6 = 2(Comment length) + 4 (Offset of start of central directory, relative to start of archive)
                final ByteBuffer temp = ByteBuffer.allocate(4);
                temp.order(ByteOrder.LITTLE_ENDIAN);
                temp.putInt((int) (centralDirStartOffset + length + 8 - (centralDirStartOffset - apkSigningBlockOffset)));
                // 8 = size of block in bytes (excluding this field) (uint64)
                temp.flip();
                fIn.write(temp.array());

            }
        } finally {
            if (fileChannel != null) {
                fileChannel.close();
            }
            if (fIn != null) {
                fIn.close();
            }
        }
    }
}