// Copyright 2009 Google Inc. All Rights Reserved.

import com.android.apicheck.*;

import java.util.*;

import org.clearsilver.HDF;

/**
 * Applies version information to the DroidDoc class model from apicheck XML
 * files. Sample usage:
 * <pre>
 *   ClassInfo[] classInfos = ...
 *
 *   SinceTagger sinceTagger = new SinceTagger()
 *   sinceTagger.addVersion("frameworks/base/api/1.xml", "Android 1.0")
 *   sinceTagger.addVersion("frameworks/base/api/2.xml", "Android 1.5")
 *   sinceTagger.tagAll(...);
 * </pre>
 */
public class SinceTagger {

    private final Map<String, String> xmlToName
            = new LinkedHashMap<String, String>();

    /**
     * Specifies the apicheck XML file and the API version it holds. Calls to
     * this method should be called in order from oldest version to newest.
     */
    public void addVersion(String file, String name) {
        xmlToName.put(file, name);
    }

    public void tagAll(ClassInfo[] classDocs) {
        // read through the XML files in order, applying their since information
        // to the Javadoc models
        for (Map.Entry<String, String> versionSpec : xmlToName.entrySet()) {
            String xmlFile = versionSpec.getKey();
            String versionName = versionSpec.getValue();
            ApiInfo specApi = new ApiCheck().parseApi(xmlFile);

            applyVersionsFromSpec(versionName, specApi, classDocs);
        }

        if (!xmlToName.isEmpty()) {
            warnForMissingVersions(classDocs);
        }
    }

    /**
     * Writes an index of the version names to {@code data}. 
     */
    public void writeVersionNames(HDF data) {
        int index = 1;
        for (String version : xmlToName.values()) {
            data.setValue("since." + index + ".name", version);
            index++;
        }
    }

    /**
     * Applies the version information to {@code classDocs} where not already
     * present.
     *
     * @param versionName the version name
     * @param specApi the spec for this version. If a symbol is in this spec, it
     *      was present in the named version
     * @param classDocs the doc model to update
     */
    private void applyVersionsFromSpec(String versionName,
            ApiInfo specApi, ClassInfo[] classDocs) {
        for (ClassInfo classDoc : classDocs) {
            com.android.apicheck.PackageInfo packageSpec
                    = specApi.getPackages().get(classDoc.containingPackage().name());

            if (packageSpec == null) {
                continue;
            }

            com.android.apicheck.ClassInfo classSpec
                    = packageSpec.allClasses().get(classDoc.name());

            if (classSpec == null) {
                continue;
            }

            versionPackage(versionName, classDoc.containingPackage());
            versionClass(versionName, classDoc);
            versionConstructors(versionName, classSpec, classDoc);
            versionFields(versionName, classSpec, classDoc);
            versionMethods(versionName, classSpec, classDoc);
        }
    }

    /**
     * Applies version information to {@code doc} where not already present.
     */
    private void versionPackage(String versionName, PackageInfo doc) {
        if (doc.getSince() == null) {
            doc.setSince(versionName);
        }
    }

    /**
     * Applies version information to {@code doc} where not already present.
     */
    private void versionClass(String versionName, ClassInfo doc) {
        if (doc.getSince() == null) {
            doc.setSince(versionName);
        }
    }

    /**
     * Applies version information from {@code spec} to {@code doc} where not
     * already present.
     */
    private void versionConstructors(String versionName,
            com.android.apicheck.ClassInfo spec, ClassInfo doc) {
        for (MethodInfo constructor : doc.constructors()) {
            if (constructor.getSince() == null
                    && spec.allConstructors().containsKey(constructor.getHashableName())) {
                constructor.setSince(versionName);
            }
        }
    }

    /**
     * Applies version information from {@code spec} to {@code doc} where not
     * already present.
     */
    private void versionFields(String versionName,
            com.android.apicheck.ClassInfo spec, ClassInfo doc) {
        for (FieldInfo field : doc.fields()) {
            if (field.getSince() == null
                    && spec.allFields().containsKey(field.name())) {
                field.setSince(versionName);
            }
        }
    }

    /**
     * Applies version information from {@code spec} to {@code doc} where not
     * already present.
     */
    private void versionMethods(String versionName,
            com.android.apicheck.ClassInfo spec, ClassInfo doc) {
        for (MethodInfo method : doc.methods()) {
            if (method.getSince() != null) {
                continue;
            }

            for (com.android.apicheck.ClassInfo superclass : spec.hierarchy()) {
                if (superclass.allMethods().containsKey(method.getHashableName())) {
                    method.setSince(versionName);
                    break;
                }
            }
        }
    }

    /**
     * Warns if any symbols are missing version information. When configured
     * properly, this will yield zero warnings because {@code apicheck}
     * guarantees that all symbols are present in the most recent API.
     */
    private void warnForMissingVersions(ClassInfo[] classDocs) {
        for (ClassInfo claz : classDocs) {
            if (claz.getSince() == null) {
                Errors.error(Errors.NO_SINCE_DATA, claz.position(),
                        "XML missing class " + claz.qualifiedName());
            }
            for (FieldInfo field : claz.fields()) {
                if (field.getSince() == null) {
                    Errors.error(Errors.NO_SINCE_DATA, field.position(),
                            "XML missing field "
                                    + claz.qualifiedName() + "#" + field .name());
                }
            }
            for (MethodInfo constructor : claz.constructors()) {
                if (constructor.getSince() == null) {
                    Errors.error(Errors.NO_SINCE_DATA, constructor.position(),
                            "XML missing constructor "
                                    + claz.qualifiedName() + "#" + constructor.getHashableName());
                }
            }
            for (MethodInfo method : claz.methods()) {
                if (method.getSince() == null) {
                    Errors.error(Errors.NO_SINCE_DATA, method.position(),
                            "XML missing method "
                                    + claz.qualifiedName() + "#" + method .getHashableName());
                }
            }
        }
    }
}
