/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * 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.android.builder.core;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.ide.common.process.BaseProcessOutputHandler;
import com.android.ide.common.process.CachedProcessOutputHandler;
import com.android.ide.common.process.ProcessException;
import com.android.ide.common.process.ProcessExecutor;
import com.android.ide.common.process.ProcessInfoBuilder;
import com.android.utils.LineCollector;
import com.google.common.base.Objects;

import java.io.File;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Class to parse an APK with aapt to gather information
 */
public class ApkInfoParser {

    private static final Pattern PATTERN = Pattern.compile(
            "^package: name='([^']+)' versionCode='([0-9]*)' versionName='([^']*)'.*$");

    @NonNull
    private final File mAaptFile;
    @NonNull
    private final ProcessExecutor mProcessExecutor;

    /**
     * Information about an APK
     */
    public static final class ApkInfo {
        @NonNull
        private final String mPackageName;
        @Nullable
        private final Integer mVersionCode;
        @Nullable
        private final String mVersionName;

        private ApkInfo(@NonNull String packageName, Integer versionCode, String versionName) {
            mPackageName = packageName;
            mVersionCode = versionCode;
            mVersionName = versionName;
        }

        @NonNull
        public String getPackageName() {
            return mPackageName;
        }

        @Nullable
        public Integer getVersionCode() {
            return mVersionCode;
        }

        @Nullable
        public String getVersionName() {
            return mVersionName;
        }

        @Override
        public String toString() {
            return Objects.toStringHelper(this)
                    .add("packageName", mPackageName)
                    .add("versionCode", mVersionCode)
                    .add("versionName", mVersionName)
                    .toString();
        }
    }

    /**
     * Constructs a new parser
     * @param aaptFile aapt file to use to gather the info
     * @param processExecutor a process Executor to call aapt
     */
    public ApkInfoParser(
            @NonNull File aaptFile,
            @NonNull ProcessExecutor processExecutor) {
        mAaptFile = aaptFile;
        mProcessExecutor = processExecutor;
    }

    /**
     * Computes and returns the info for an APK
     * @param apkFile the APK to parse
     * @return a non-null ApkInfo object.
     * @throws ProcessException
     */
    @NonNull
    public ApkInfo parseApk(@NonNull File apkFile)
            throws ProcessException {

        if (!mAaptFile.isFile()) {
            throw new IllegalStateException(
                    "aapt is missing from location: " + mAaptFile.getAbsolutePath());
        }

        return getApkInfo(getAaptOutput(apkFile));
    }

    /**
     * Parses the aapt output and returns an ApkInfo object.
     * @param aaptOutput the aapt output as a list of lines.
     * @return an ApkInfo object.
     */
    @VisibleForTesting
    @NonNull
    static ApkInfo getApkInfo(@NonNull List<String> aaptOutput) {

        String pkgName = null, versionCode = null, versionName = null;

        for (String line : aaptOutput) {
            Matcher m = PATTERN.matcher(line);
            if (m.matches()) {
                pkgName = m.group(1);
                versionCode = m.group(2);
                versionName = m.group(3);
                break;
            }
        }

        if (pkgName == null) {
            throw new RuntimeException("Failed to find apk information with aapt");
        }

        Integer intVersionCode = null;
        try {
            intVersionCode = Integer.parseInt(versionCode);
        } catch(NumberFormatException ignore) {
            // leave the version code as null.
        }

        return new ApkInfo(pkgName, intVersionCode, versionName);
    }

    /**
     * Returns the aapt output.
     * @param apkFile the apk file to call aapt on.
     * @return the output as a list of files.
     * @throws ProcessException
     */
    @NonNull
    private List<String> getAaptOutput(@NonNull File apkFile)
            throws ProcessException {
        ProcessInfoBuilder builder = new ProcessInfoBuilder();

        builder.setExecutable(mAaptFile);
        builder.addArgs("dump", "badging", apkFile.getPath());

        CachedProcessOutputHandler processOutputHandler = new CachedProcessOutputHandler();

        mProcessExecutor.execute(
                builder.createProcess(), processOutputHandler)
                .rethrowFailure().assertNormalExitValue();

        BaseProcessOutputHandler.BaseProcessOutput output = processOutputHandler.getProcessOutput();

        LineCollector lineCollector = new LineCollector();
        output.processStandardOutputLines(lineCollector);
        return lineCollector.getResult();
    }
}
