/*
 * Copyright (C) 2013 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.build.gradle.internal.tasks;

import com.android.annotations.NonNull;
import com.android.build.gradle.internal.variant.BaseVariantData;
import com.android.builder.model.SigningConfig;
import com.android.ide.common.signing.CertificateInfo;
import com.android.ide.common.signing.KeystoreHelper;
import com.android.ide.common.signing.KeytoolException;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import org.gradle.api.DefaultTask;
import org.gradle.api.tasks.TaskAction;
import org.gradle.internal.logging.text.StyledTextOutput;
import org.gradle.internal.logging.text.StyledTextOutputFactory;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.text.DateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import static org.gradle.internal.logging.text.StyledTextOutput.Style.Description;
import static org.gradle.internal.logging.text.StyledTextOutput.Style.Failure;
import static org.gradle.internal.logging.text.StyledTextOutput.Style.Identifier;
import static org.gradle.internal.logging.text.StyledTextOutput.Style.Normal;

/**
 * Report tasks displaying the signing information for all variants.
 */
public class SigningReportTask extends DefaultTask {

    private Set<BaseVariantData> variants = Sets.newHashSet();

    @TaskAction
    public void generate() throws IOException {

        StyledTextOutput textOutput = getServices().get(
                StyledTextOutputFactory.class).create(getClass());

        Map<SigningConfig, SigningInfo> cache = Maps.newHashMap();

        for (BaseVariantData variant : variants) {
            textOutput.withStyle(Identifier).text("Variant: ");
            textOutput.withStyle(Description).text(variant.getName());
            textOutput.println();

            // get the data
            SigningConfig signingConfig = variant.getVariantConfiguration().getSigningConfig();
            if (signingConfig == null) {
                textOutput.withStyle(Identifier).text("Config: ");
                textOutput.withStyle(Normal).text("none");
                textOutput.println();
            } else {
                SigningInfo signingInfo = getSigningInfo(signingConfig, cache);


                textOutput.withStyle(Identifier).text("Config: ");
                textOutput.withStyle(Description).text(signingConfig.getName());
                textOutput.println();

                textOutput.withStyle(Identifier).text("Store: ");
                textOutput.withStyle(Description).text(signingConfig.getStoreFile());
                textOutput.println();

                textOutput.withStyle(Identifier).text("Alias: ");
                textOutput.withStyle(Description).text(signingConfig.getKeyAlias());
                textOutput.println();

                if (signingInfo.isValid()) {
                    if (signingInfo.error != null) {
                        textOutput.withStyle(Identifier).text("Error: ");
                        textOutput.withStyle(Failure).text(signingInfo.error);
                        textOutput.println();
                    } else {
                        textOutput.withStyle(Identifier).text("MD5: ");
                        textOutput.withStyle(Description).text(signingInfo.md5);
                        textOutput.println();

                        textOutput.withStyle(Identifier).text("SHA1: ");
                        textOutput.withStyle(Description).text(signingInfo.sha1);
                        textOutput.println();

                        textOutput.withStyle(Identifier).text("Valid until: ");
                        DateFormat df = DateFormat.getDateInstance(DateFormat.FULL);
                        textOutput.withStyle(Description).text(df.format(signingInfo.notAfter));
                        textOutput.println();
                    }
                }
            }

            textOutput.withStyle(Normal).text("----------");
            textOutput.println();
        }
    }

    /**
     * Sets the configurations to generate the report for.
     */
    public void setVariants(@NonNull Collection<? extends BaseVariantData> variants) {
        this.variants.addAll(variants);
    }

    private static SigningInfo getSigningInfo(
            @NonNull SigningConfig signingConfig,
            @NonNull Map<SigningConfig, SigningInfo> cache) {
        SigningInfo signingInfo = cache.get(signingConfig);

        if (signingInfo == null) {
            signingInfo = new SigningInfo();

            if (signingConfig.isSigningReady()) {
                try {
                    CertificateInfo certificateInfo = KeystoreHelper.getCertificateInfo(
                            signingConfig.getStoreType(), signingConfig.getStoreFile(),
                            signingConfig.getStorePassword(), signingConfig.getKeyPassword(),
                            signingConfig.getKeyAlias());
                    if (certificateInfo != null) {
                        signingInfo.md5 = getFingerprint(certificateInfo.getCertificate(), "MD5");
                        signingInfo.sha1 = getFingerprint(certificateInfo.getCertificate(), "SHA1");
                        signingInfo.notAfter = certificateInfo.getCertificate().getNotAfter();
                    }
                } catch (KeytoolException e) {
                    signingInfo.error = e.getMessage();
                } catch (FileNotFoundException e) {
                    signingInfo.error = "Missing keystore";
                }
            }

            cache.put(signingConfig, signingInfo);
        }

        return signingInfo;
    }

    private static final class SigningInfo {
        String md5;
        String sha1;
        Date notAfter;
        String error;

        boolean isValid() {
            return md5 != null || error != null;
        }
    }

    /**
     * Returns the {@link Certificate} fingerprint as returned by <code>keytool</code>.
     */
    public static String getFingerprint(Certificate cert, String hashAlgorithm) {
        if (cert == null) {
            return null;
        }
        try {
            MessageDigest digest = MessageDigest.getInstance(hashAlgorithm);
            return toHexadecimalString(digest.digest(cert.getEncoded()));
        } catch(NoSuchAlgorithmException e) {
            // ignore
        } catch(CertificateEncodingException e) {
            // ignore
        }
        return null;
    }

    private static String toHexadecimalString(byte[] value) {
        StringBuilder sb = new StringBuilder();
        int len = value.length;
        for (int i = 0; i < len; i++) {
            int num = ((int) value[i]) & 0xff;
            if (num < 0x10) {
                sb.append('0');
            }
            sb.append(Integer.toHexString(num));
            if (i < len - 1) {
                sb.append(':');
            }
        }
        return sb.toString().toUpperCase(Locale.US);
    }
}
