/*
 * Tencent is pleased to support the open source community by making Tinker available.
 *
 * Copyright (C) 2016 THL A29 Limited, a Tencent company. All rights reserved.
 *
 * Licensed under the BSD 3-Clause License (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 *
 * https://opensource.org/licenses/BSD-3-Clause
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is
 * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.tencent.tinker.build.decoder;


import com.tencent.tinker.build.patch.Configuration;
import com.tencent.tinker.build.util.FileOperation;
import com.tencent.tinker.build.util.Logger;
import com.tencent.tinker.build.util.MD5;
import com.tencent.tinker.build.util.TinkerPatchException;
import com.tencent.tinker.build.util.TypedValue;
import com.tencent.tinker.build.util.Utils;

import java.io.File;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.regex.Matcher;

/**
 * Created by zhangshaowen on 16/3/15.
 */
public class ApkDecoder extends BaseDecoder {
    private final File mOldApkDir;
    private final File mNewApkDir;

    private final ManifestDecoder      manifestDecoder;
    private final UniqueDexDiffDecoder dexPatchDecoder;
    private final SoDiffDecoder        soPatchDecoder;
    private final ResDiffDecoder       resPatchDecoder;
    private final ArkHotDecoder        arkHotDecoder;

    /**
     * if resource's file is also contain in dex or library pattern,
     * they won't change in new resources' apk, and we will just warn you.
     */
    ArrayList<File> resDuplicateFiles;

    public ApkDecoder(Configuration config) throws IOException {
        super(config);
        this.mNewApkDir = config.mTempUnzipNewDir;
        this.mOldApkDir = config.mTempUnzipOldDir;

        this.manifestDecoder = new ManifestDecoder(config);

        //put meta files in assets
        String prePath = TypedValue.FILE_ASSETS + File.separator;
        dexPatchDecoder = new UniqueDexDiffDecoder(config, prePath + TypedValue.DEX_META_FILE, TypedValue.DEX_LOG_FILE);
        soPatchDecoder = new SoDiffDecoder(config, prePath + TypedValue.SO_META_FILE, TypedValue.SO_LOG_FILE);
        resPatchDecoder = new ResDiffDecoder(config, prePath + TypedValue.RES_META_TXT, TypedValue.RES_LOG_FILE);

        arkHotDecoder = new ArkHotDecoder(config, prePath + TypedValue.ARKHOT_META_TXT);
        Logger.d("config: " + config.mArkHotPatchPath + " " + config.mArkHotPatchName + prePath + TypedValue.ARKHOT_META_TXT);
        resDuplicateFiles = new ArrayList<>();
    }

    private void unzipApkFile(File file, File destFile) throws TinkerPatchException, IOException {
        String apkName = file.getName();
        if (!apkName.endsWith(TypedValue.FILE_APK)) {
            throw new TinkerPatchException(
                String.format("input apk file path must end with .apk, yours %s\n", apkName)
            );
        }

        String destPath = destFile.getAbsolutePath();
        Logger.d("UnZipping apk to %s", destPath);
        FileOperation.unZipAPk(file.getAbsoluteFile().getAbsolutePath(), destPath);

    }

    private void unzipApkFiles(File oldFile, File newFile) throws IOException, TinkerPatchException {
        unzipApkFile(oldFile, this.mOldApkDir);
        unzipApkFile(newFile, this.mNewApkDir);
    }

    private void writeToLogFile(File oldFile, File newFile) throws IOException {
        String line1 = "old apk: " + oldFile.getName() + ", size=" + FileOperation.getFileSizes(oldFile) + ", md5=" + MD5.getMD5(oldFile);
        String line2 = "new apk: " + newFile.getName() + ", size=" + FileOperation.getFileSizes(newFile) + ", md5=" + MD5.getMD5(newFile);
        Logger.d("Analyze old and new apk files1:");
        Logger.d(line1);
        Logger.d(line2);
        Logger.d("");
    }

    @Override
    public void onAllPatchesStart() throws IOException, TinkerPatchException {
        manifestDecoder.onAllPatchesStart();
        dexPatchDecoder.onAllPatchesStart();
        soPatchDecoder.onAllPatchesStart();
        resPatchDecoder.onAllPatchesStart();
    }

    public boolean patch(File oldFile, File newFile) throws Exception {
        writeToLogFile(oldFile, newFile);
        //check manifest change first
        manifestDecoder.patch(oldFile, newFile);

        unzipApkFiles(oldFile, newFile);

        Files.walkFileTree(mNewApkDir.toPath(), new ApkFilesVisitor(config, mNewApkDir.toPath(), mOldApkDir.toPath(), dexPatchDecoder, soPatchDecoder, resPatchDecoder));

        // get all duplicate resource file
        for (File duplicateRes : resDuplicateFiles) {
            // resPatchDecoder.patch(duplicateRes, null);
            Logger.e("Warning: res file %s is also match at dex or library pattern, "
                + "we treat it as unchanged in the new resource_out.zip", getRelativePathStringToOldFile(duplicateRes));
        }

        soPatchDecoder.onAllPatchesEnd();
        dexPatchDecoder.onAllPatchesEnd();
        manifestDecoder.onAllPatchesEnd();
        resPatchDecoder.onAllPatchesEnd();
        arkHotDecoder.onAllPatchesEnd();

        //clean resources
        dexPatchDecoder.clean();
        soPatchDecoder.clean();
        resPatchDecoder.clean();
        arkHotDecoder.clean();

        return true;
    }

    @Override
    public void onAllPatchesEnd() throws IOException, TinkerPatchException {
    }

    class ApkFilesVisitor extends SimpleFileVisitor<Path> {
        BaseDecoder     dexDecoder;
        BaseDecoder     soDecoder;
        BaseDecoder     resDecoder;
        Configuration   config;
        Path            newApkPath;
        Path            oldApkPath;

        ApkFilesVisitor(Configuration config, Path newPath, Path oldPath, BaseDecoder dex, BaseDecoder so, BaseDecoder resDecoder) {
            this.config = config;
            this.dexDecoder = dex;
            this.soDecoder = so;
            this.resDecoder = resDecoder;
            this.newApkPath = newPath;
            this.oldApkPath = oldPath;
        }

        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {

            Path relativePath = newApkPath.relativize(file);

            Path oldPath = oldApkPath.resolve(relativePath);

            File oldFile = null;
            //is a new file?!
            if (oldPath.toFile().exists()) {
                oldFile = oldPath.toFile();
            }
            String patternKey = relativePath.toString().replace("\\", "/");

            if (Utils.checkFileInPattern(config.mDexFilePattern, patternKey)) {
                //also treat duplicate file as unchanged
                if (Utils.checkFileInPattern(config.mResFilePattern, patternKey) && oldFile != null) {
                    resDuplicateFiles.add(oldFile);
                }

                try {
                    dexDecoder.patch(oldFile, file.toFile());
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                return FileVisitResult.CONTINUE;
            }
            if (Utils.checkFileInPattern(config.mSoFilePattern, patternKey)) {
                //also treat duplicate file as unchanged
                if (Utils.checkFileInPattern(config.mResFilePattern, patternKey) && oldFile != null) {
                    resDuplicateFiles.add(oldFile);
                }

                // For abi validation purpose.
                if (file.toFile().exists()) {
                    final String newAbi = getAbiFromPath(file.toFile().getAbsolutePath());
                    if (newAbi != null) {
                        final File oldSoPathWithNewAbi = new File(oldApkPath.toFile(), "lib/" + newAbi);
                        if (!oldSoPathWithNewAbi.exists()) {
                            throw new UnsupportedOperationException("Tinker does not support to add new ABI: " + newAbi
                                    + ", related new so: " + file.toFile().getAbsolutePath());
                        }
                    }
                }

                try {
                    soDecoder.patch(oldFile, file.toFile());
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                return FileVisitResult.CONTINUE;
            }
            if (Utils.checkFileInPattern(config.mResFilePattern, patternKey)) {
                try {
                    resDecoder.patch(oldFile, file.toFile());
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                return FileVisitResult.CONTINUE;
            }
            return FileVisitResult.CONTINUE;
        }

        private String getAbiFromPath(String path) {
            path = path.replaceAll(Matcher.quoteReplacement(File.separator), "/");
            final int prefixPos = path.indexOf("/lib/");
            if (prefixPos < 0) {
                return null;
            }
            final int suffixPos = path.indexOf("/", prefixPos + 5);
            if (suffixPos < 0) {
                return null;
            }
            return path.substring(prefixPos + 5, suffixPos);
        }
    }
}
