/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
 *
 * 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.
 */

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

/**
 * Gathers statistics about attribute usage in layout files. This is how the "topAttrs"
 * attributes listed in ADT's extra-view-metadata.xml (which drives the common attributes
 * listed in the top of the context menu) is determined by running this script on a body
 * of sample layout code.
 * <p>
 * This program takes one or more directory paths, and then it searches all of them recursively
 * for layout files that are not in folders containing the string "test", and computes and
 * prints frequency statistics.
 */
public class Analyzer {
    /** Number of attributes to print for each view */
    public static final int ATTRIBUTE_COUNT = 6;
    /** Separate out any attributes that constitute less than N percent of the total */
    public static final int THRESHOLD = 10; // percent

    private List<File> mDirectories;
    private File mCurrentFile;

    /** Map from view id to map from attribute to frequency count */
    private Map<String, Map<String, Usage>> mFrequencies =
            new HashMap<String, Map<String, Usage>>(100);

    private Map<String, Map<String, Usage>> mLayoutAttributeFrequencies =
            new HashMap<String, Map<String, Usage>>(100);

    private Map<String, String> mTopAttributes = new HashMap<String, String>(100);
    private Map<String, String> mTopLayoutAttributes = new HashMap<String, String>(100);

    private int mFileVisitCount;
    private int mLayoutFileCount;
    private File mXmlMetadataFile;

    private Analyzer(List<File> directories, File xmlMetadataFile) {
        mDirectories = directories;
        mXmlMetadataFile = xmlMetadataFile;
    }

    public static void main(String[] args) {
        if (args.length < 1) {
            System.err.println("Usage: " + Analyzer.class.getSimpleName()
                    + " <directory1> [directory2 [directory3 ...]]\n");
            System.err.println("Recursively scans for layouts in the given directory and");
            System.err.println("computes statistics about attribute frequencies.");
            System.exit(-1);
        }

        File metadataFile = null;
        List<File> directories = new ArrayList<File>();
        for (int i = 0, n = args.length; i < n; i++) {
            String arg = args[i];

            // The -metadata flag takes a pointer to an ADT extra-view-metadata.xml file
            // and attempts to insert topAttrs attributes into it (and saves it as same
            // file +.mod as an extension). This isn't listed on the usage flag because
            // it's pretty brittle and requires some manual fixups to the file afterwards.
            if (arg.equals("-metadata")) {
                i++;
                File file = new File(args[i]);
                if (!file.exists()) {
                    System.err.println(file.getName() + " does not exist");
                    System.exit(-5);
                }
                if (!file.isFile() || !file.getName().endsWith(".xml")) {
                    System.err.println(file.getName() + " must be an XML file");
                    System.exit(-4);
                }
                metadataFile = file;
                continue;
            }
            File directory = new File(arg);
            if (!directory.exists()) {
                System.err.println(directory.getName() + " does not exist");
                System.exit(-2);
            }

            if (!directory.isDirectory()) {
                System.err.println(directory.getName() + " is not a directory");
                System.exit(-3);
            }

            directories.add(directory);
        }

        new Analyzer(directories, metadataFile).analyze();
    }

    private void analyze() {
        for (File directory : mDirectories) {
            scanDirectory(directory);
        }
        printStatistics();

        if (mXmlMetadataFile != null) {
            printMergedMetadata();
        }
    }

    private void scanDirectory(File directory) {
        File[] files = directory.listFiles();
        if (files == null) {
            return;
        }

        for (File file : files) {
            mFileVisitCount++;
            if (mFileVisitCount % 50000 == 0) {
                System.out.println("Analyzed " + mFileVisitCount + " files...");
            }

            if (file.isFile()) {
                scanFile(file);
            } else if (file.isDirectory()) {
                // Skip stuff related to tests
                if (file.getName().contains("test")) {
                    continue;
                }

                // Recurse over subdirectories
                scanDirectory(file);
            }
        }
    }

    private void scanFile(File file) {
        if (file.getName().endsWith(".xml")) {
            File parent = file.getParentFile();
            if (parent.getName().startsWith("layout")) {
                analyzeLayout(file);
            }
        }

    }

    private void analyzeLayout(File file) {
        mCurrentFile = file;
        mLayoutFileCount++;
        Document document = null;
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        InputSource is = new InputSource(new StringReader(readFile(file)));
        try {
            factory.setNamespaceAware(true);
            factory.setValidating(false);
            DocumentBuilder builder = factory.newDocumentBuilder();
            document = builder.parse(is);

            analyzeDocument(document);

        } catch (ParserConfigurationException e) {
            // pass -- ignore files we can't parse
        } catch (SAXException e) {
            // pass -- ignore files we can't parse
        } catch (IOException e) {
            // pass -- ignore files we can't parse
        }
    }


    private void analyzeDocument(Document document) {
        analyzeElement(document.getDocumentElement());
    }

    private void analyzeElement(Element element) {
        if (element.getTagName().equals("item")) {
            // Resource files shouldn't be in the layout/ folder but I came across
            // some cases
            System.out.println("Warning: found <item> tag in a layout file in "
                    + mCurrentFile.getPath());
            return;
        }

        countAttributes(element);
        countLayoutAttributes(element);

        // Recurse over children
        NodeList childNodes = element.getChildNodes();
        for (int i = 0, n = childNodes.getLength(); i < n; i++) {
            Node child = childNodes.item(i);
            if (child.getNodeType() == Node.ELEMENT_NODE) {
                analyzeElement((Element) child);
            }
        }
    }

    private void countAttributes(Element element) {
        String tag = element.getTagName();
        Map<String, Usage> attributeMap = mFrequencies.get(tag);
        if (attributeMap == null) {
            attributeMap = new HashMap<String, Usage>(70);
            mFrequencies.put(tag, attributeMap);
        }

        NamedNodeMap attributes = element.getAttributes();
        for (int i = 0, n = attributes.getLength(); i < n; i++) {
            Node attribute = attributes.item(i);
            String name = attribute.getNodeName();

            if (name.startsWith("android:layout_")) {
                // Skip layout attributes; they are a function of the parent layout that this
                // view is embedded within, not the view itself.
                // TODO: Consider whether we should incorporate this info or make statistics
                // about that as well?
                continue;
            }

            if (name.equals("android:id")) {
                // Skip ids: they are (mostly) unrelated to the view type and the tool
                // already offers id editing prominently
                continue;
            }

            if (name.startsWith("xmlns:")) {
                // Unrelated to frequency counts
                continue;
            }

            Usage usage = attributeMap.get(name);
            if (usage == null) {
                usage = new Usage(name);
            } else {
                usage.incrementCount();
            }
            attributeMap.put(name, usage);
        }
    }

    private void countLayoutAttributes(Element element) {
        String parentTag = element.getParentNode().getNodeName();
        Map<String, Usage> attributeMap = mLayoutAttributeFrequencies.get(parentTag);
        if (attributeMap == null) {
            attributeMap = new HashMap<String, Usage>(70);
            mLayoutAttributeFrequencies.put(parentTag, attributeMap);
        }

        NamedNodeMap attributes = element.getAttributes();
        for (int i = 0, n = attributes.getLength(); i < n; i++) {
            Node attribute = attributes.item(i);
            String name = attribute.getNodeName();

            if (!name.startsWith("android:layout_")) {
                continue;
            }

            // Skip layout_width and layout_height; they are mandatory in all but GridLayout so not
            // very interesting
            if (name.equals("android:layout_width") || name.equals("android:layout_height")) {
                continue;
            }

            Usage usage = attributeMap.get(name);
            if (usage == null) {
                usage = new Usage(name);
            } else {
                usage.incrementCount();
            }
            attributeMap.put(name, usage);
        }
    }

    // Copied from AdtUtils
    private static String readFile(File file) {
        try {
            return readFile(new FileReader(file));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        return null;
    }

    private static String readFile(Reader inputStream) {
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(inputStream);
            StringBuilder sb = new StringBuilder(2000);
            while (true) {
                int c = reader.read();
                if (c == -1) {
                    return sb.toString();
                } else {
                    sb.append((char)c);
                }
            }
        } catch (IOException e) {
            // pass -- ignore files we can't read
        } finally {
            try {
                if (reader != null) {
                    reader.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return null;
    }

    private void printStatistics() {
        System.out.println("Analyzed " + mLayoutFileCount
                + " layouts (in a directory trees containing " + mFileVisitCount + " files)");
        System.out.println("Top " + ATTRIBUTE_COUNT
                + " for each view (excluding layout_ attributes) :");
        System.out.println("\n");
        System.out.println(" Rank    Count    Share  Attribute");
        System.out.println("=========================================================");
        List<String> views = new ArrayList<String>(mFrequencies.keySet());
        Collections.sort(views);
        for (String view : views) {
            String top = processUageMap(view, mFrequencies.get(view));
            if (top != null) {
                mTopAttributes.put(view,  top);
            }
        }

        System.out.println("\n\n\nTop " + ATTRIBUTE_COUNT + " layout attributes (excluding "
                + "mandatory layout_width and layout_height):");
        System.out.println("\n");
        System.out.println(" Rank    Count    Share  Attribute");
        System.out.println("=========================================================");
        views = new ArrayList<String>(mLayoutAttributeFrequencies.keySet());
        Collections.sort(views);
        for (String view : views) {
            String top = processUageMap(view, mLayoutAttributeFrequencies.get(view));
            if (top != null) {
                mTopLayoutAttributes.put(view,  top);
            }
        }
    }

    private static String processUageMap(String view, Map<String, Usage> map) {
        if (map == null) {
            return null;
        }

        if (view.indexOf('.') != -1 && !view.startsWith("android.")) {
            // Skip custom views
            return null;
        }

        List<Usage> values = new ArrayList<Usage>(map.values());
        if (values.size() == 0) {
            return null;
        }

        Collections.sort(values);
        int totalCount = 0;
        for (Usage usage : values) {
            totalCount += usage.count;
        }

        System.out.println("\n<" + view + ">:");
        if (view.equals("#document")) {
            System.out.println("(Set on root tag, probably intended for included context)");
        }

        int place = 1;
        int count = 0;
        int prevCount = -1;
        float prevPercentage = 0f;
        StringBuilder sb = new StringBuilder();
        for (Usage usage : values) {
            if (count++ >= ATTRIBUTE_COUNT && usage.count < prevCount) {
                break;
            }

            float percentage = 100 * usage.count/(float)totalCount;
            if (percentage < THRESHOLD && prevPercentage >= THRESHOLD) {
                System.out.println("  -----Less than 10%-------------------------------------");
            }
            System.out.printf("  %1d.    %5d    %5.1f%%  %s\n", place, usage.count,
                    percentage, usage.attribute);

            prevPercentage = percentage;
            if (prevCount != usage.count) {
                prevCount = usage.count;
                place++;
            }

            if (percentage >= THRESHOLD /*&& usage.count > 1*/) { // 1:Ignore when not enough data?
                if (sb.length() > 0) {
                    sb.append(',');
                }
                String name = usage.attribute;
                if (name.startsWith("android:")) {
                    name = name.substring("android:".length());
                }
                sb.append(name);
            }
        }

        return sb.length() > 0 ? sb.toString() : null;
    }

    private void printMergedMetadata() {
        assert mXmlMetadataFile != null;
        String metadata = readFile(mXmlMetadataFile);
        if (metadata == null || metadata.length() == 0) {
            System.err.println("Invalid metadata file");
            System.exit(-6);
        }

        System.err.flush();
        System.out.println("\n\nUpdating layout metadata file...");
        System.out.flush();

        StringBuilder sb = new StringBuilder((int) (2 * mXmlMetadataFile.length()));
        String[] lines = metadata.split("\n");
        for (int i = 0; i < lines.length; i++) {
            String line = lines[i];
            sb.append(line).append('\n');
            int classIndex = line.indexOf("class=\"");
            if (classIndex != -1) {
                int start = classIndex + "class=\"".length();
                int end = line.indexOf('"', start + 1);
                if (end != -1) {
                    String view = line.substring(start, end);
                    if (view.startsWith("android.widget.")) {
                        view = view.substring("android.widget.".length());
                    } else if (view.startsWith("android.view.")) {
                        view = view.substring("android.view.".length());
                    } else if (view.startsWith("android.webkit.")) {
                        view = view.substring("android.webkit.".length());
                    }
                    String top = mTopAttributes.get(view);
                    if (top == null) {
                        System.err.println("Warning: No frequency data for view " + view);
                    } else {
                        sb.append(line.substring(0, classIndex)); // Indentation

                        sb.append("topAttrs=\"");
                        sb.append(top);
                        sb.append("\"\n");
                    }

                    top = mTopLayoutAttributes.get(view);
                    if (top != null) {
                        // It's a layout attribute
                        sb.append(line.substring(0, classIndex)); // Indentation

                        sb.append("topLayoutAttrs=\"");
                        sb.append(top);
                        sb.append("\"\n");
                    }
                }
            }
        }

        System.out.println("\nTop attributes:");
        System.out.println("--------------------------");
        List<String> views = new ArrayList<String>(mTopAttributes.keySet());
        Collections.sort(views);
        for (String view : views) {
            String top = mTopAttributes.get(view);
            System.out.println(view + ": " + top);
        }

        System.out.println("\nTop layout attributes:");
        System.out.println("--------------------------");
        views = new ArrayList<String>(mTopLayoutAttributes.keySet());
        Collections.sort(views);
        for (String view : views) {
            String top = mTopLayoutAttributes.get(view);
            System.out.println(view + ": " + top);
        }

        System.out.println("\nModified XML metadata file:\n");
        String newContent = sb.toString();
        File output = new File(mXmlMetadataFile.getParentFile(), mXmlMetadataFile.getName() + ".mod");
        if (output.exists()) {
            output.delete();
        }
        try {
            BufferedWriter writer = new BufferedWriter(new FileWriter(output));
            writer.write(newContent);
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("Done - wrote " + output.getPath());
    }

    private static class Usage implements Comparable<Usage> {
        public String attribute;
        public int count;


        public Usage(String attribute) {
            super();
            this.attribute = attribute;

            count = 1;
        }

        public void incrementCount() {
            count++;
        }

        public int compareTo(Usage o) {
            // Sort by decreasing frequency, then sort alphabetically
            int frequencyDelta = o.count - count;
            if (frequencyDelta != 0) {
                return frequencyDelta;
            } else {
                return attribute.compareTo(o.attribute);
            }
        }

        @Override
        public String toString() {
            return attribute + ": " + count;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((attribute == null) ? 0 : attribute.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            Usage other = (Usage) obj;
            if (attribute == null) {
                if (other.attribute != null)
                    return false;
            } else if (!attribute.equals(other.attribute))
                return false;
            return true;
        }
    }
}
