/*
 * Copyright (C) 2010 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.
 */

package com.android.ide.eclipse.adt.internal.build;

import com.android.ide.eclipse.adt.AdtConstants;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.text.FindReplaceDocumentAdapter;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.ui.editors.text.TextFileDocumentProvider;
import org.eclipse.ui.texteditor.IDocumentProvider;

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

public final class AaptParser {

    // TODO: rename the pattern to something that makes sense + javadoc comments.
    /**
     * Single line aapt warning for skipping files.<br>
     * "  (skipping hidden file '&lt;file path&gt;'"
     */
    private final static Pattern sPattern0Line1 = Pattern.compile(
            "^\\s+\\(skipping hidden file\\s'(.*)'\\)$"); //$NON-NLS-1$

    /**
     * First line of dual line aapt error.<br>
     * "ERROR at line &lt;line&gt;: &lt;error&gt;"<br>
     * " (Occurred while parsing &lt;path&gt;)"
     */
    private final static Pattern sPattern1Line1 = Pattern.compile(
            "^ERROR\\s+at\\s+line\\s+(\\d+):\\s+(.*)$"); //$NON-NLS-1$
    /**
     * Second line of dual line aapt error.<br>
     * "ERROR at line &lt;line&gt;: &lt;error&gt;"<br>
     * " (Occurred while parsing &lt;path&gt;)"<br>
     * @see #sPattern1Line1
     */
    private final static Pattern sPattern1Line2 = Pattern.compile(
            "^\\s+\\(Occurred while parsing\\s+(.*)\\)$");  //$NON-NLS-1$
    /**
     * First line of dual line aapt error.<br>
     * "ERROR: &lt;error&gt;"<br>
     * "Defined at file &lt;path&gt; line &lt;line&gt;"
     */
    private final static Pattern sPattern2Line1 = Pattern.compile(
            "^ERROR:\\s+(.+)$"); //$NON-NLS-1$
    /**
     * Second line of dual line aapt error.<br>
     * "ERROR: &lt;error&gt;"<br>
     * "Defined at file &lt;path&gt; line &lt;line&gt;"<br>
     * @see #sPattern2Line1
     */
    private final static Pattern sPattern2Line2 = Pattern.compile(
            "Defined\\s+at\\s+file\\s+(.+)\\s+line\\s+(\\d+)"); //$NON-NLS-1$
    /**
     * Single line aapt error<br>
     * "&lt;path&gt; line &lt;line&gt;: &lt;error&gt;"
     */
    private final static Pattern sPattern3Line1 = Pattern.compile(
            "^(.+)\\sline\\s(\\d+):\\s(.+)$"); //$NON-NLS-1$
    /**
     * First line of dual line aapt error.<br>
     * "ERROR parsing XML file &lt;path&gt;"<br>
     * "&lt;error&gt; at line &lt;line&gt;"
     */
    private final static Pattern sPattern4Line1 = Pattern.compile(
            "^Error\\s+parsing\\s+XML\\s+file\\s(.+)$"); //$NON-NLS-1$
    /**
     * Second line of dual line aapt error.<br>
     * "ERROR parsing XML file &lt;path&gt;"<br>
     * "&lt;error&gt; at line &lt;line&gt;"<br>
     * @see #sPattern4Line1
     */
    private final static Pattern sPattern4Line2 = Pattern.compile(
            "^(.+)\\s+at\\s+line\\s+(\\d+)$"); //$NON-NLS-1$

    /**
     * Single line aapt warning<br>
     * "&lt;path&gt;:&lt;line&gt;: &lt;error&gt;"
     */
    private final static Pattern sPattern5Line1 = Pattern.compile(
            "^(.+?):(\\d+):\\s+WARNING:(.+)$"); //$NON-NLS-1$

    /**
     * Single line aapt error<br>
     * "&lt;path&gt;:&lt;line&gt;: &lt;error&gt;"
     */
    private final static Pattern sPattern6Line1 = Pattern.compile(
            "^(.+?):(\\d+):\\s+(.+)$"); //$NON-NLS-1$

    /**
     * 4 line aapt error<br>
     * "ERROR: 9-path image &lt;path&gt; malformed"<br>
     * Line 2 and 3 are taken as-is while line 4 is ignored (it repeats with<br>
     * 'ERROR: failure processing &lt;path&gt;)
     */
    private final static Pattern sPattern7Line1 = Pattern.compile(
            "^ERROR:\\s+9-patch\\s+image\\s+(.+)\\s+malformed\\.$"); //$NON-NLS-1$

    private final static Pattern sPattern8Line1 = Pattern.compile(
            "^(invalid resource directory name): (.*)$"); //$NON-NLS-1$

    /**
     * Portion of the error message which states the context in which the error occurred,
     * such as which property was being processed and what the string value was that
     * caused the error.
     * <p>
     * Example:
     * error: No resource found that matches the given name (at 'text' with value '@string/foo')
     */
    private static final Pattern sValueRangePattern =
        Pattern.compile("\\(at '(.+)' with value '(.*)'\\)"); //$NON-NLS-1$


    /**
     * Portion of error message which points to the second occurrence of a repeated resource
     * definition.
     * <p>
     * Example:
     * error: Resource entry repeatedStyle1 already has bag item android:gravity.
     */
    private static final Pattern sRepeatedRangePattern =
        Pattern.compile("Resource entry (.+) already has bag item (.+)\\."); //$NON-NLS-1$

    /**
     * Error message emitted when aapt skips a file because for example it's name is
     * invalid, such as a layout file name which starts with _.
     */
    private static final Pattern sSkippingPattern =
        Pattern.compile("    \\(skipping .+ .+ '(.*)'\\)"); //$NON-NLS-1$

    /**
     * Suffix of error message which points to the first occurrence of a repeated resource
     * definition.
     * Example:
     * Originally defined here.
     */
    private static final String ORIGINALLY_DEFINED_MSG = "Originally defined here."; //$NON-NLS-1$

    /**
     * Portion of error message which points to the second occurrence of a repeated resource
     * definition.
     * <p>
     * Example:
     * error: Resource entry repeatedStyle1 already has bag item android:gravity.
     */
    private static final Pattern sNoResourcePattern =
        Pattern.compile("No resource found that matches the given name: attr '(.+)'\\."); //$NON-NLS-1$

    /**
     * Portion of error message which points to a missing required attribute in a
     * resource definition.
     * <p>
     * Example:
     * error: error: A 'name' attribute is required for <style>
     */
    private static final Pattern sRequiredPattern =
        Pattern.compile("A '(.+)' attribute is required for <(.+)>"); //$NON-NLS-1$

    /**
     * 2 line aapt error<br>
     * "ERROR: Invalid configuration: foo"<br>
     * "                              ^^^"<br>
     * There's no need to parse the 2nd line.
     */
    private final static Pattern sPattern9Line1 = Pattern.compile(
            "^Invalid configuration: (.+)$"); //$NON-NLS-1$

    /**
     * Parse the output of aapt and mark the incorrect file with error markers
     *
     * @param results the output of aapt
     * @param project the project containing the file to mark
     * @return true if the parsing failed, false if success.
     */
    public static boolean parseOutput(List<String> results, IProject project) {
        int size = results.size();
        if (size > 0) {
            return parseOutput(results.toArray(new String[size]), project);
        }

        return true;
    }

    /**
     * Parse the output of aapt and mark the incorrect file with error markers
     *
     * @param results the output of aapt
     * @param project the project containing the file to mark
     * @return true if the parsing failed, false if success.
     */
    public static boolean parseOutput(String[] results, IProject project) {
        // nothing to parse? just return false;
        if (results.length == 0) {
            return false;
        }

        // get the root of the project so that we can make IFile from full
        // file path
        String osRoot = project.getLocation().toOSString();

        Matcher m;

        for (int i = 0; i < results.length ; i++) {
            String p = results[i];

            m = sPattern0Line1.matcher(p);
            if (m.matches()) {
                // we ignore those (as this is an ignore message from aapt)
                continue;
            }

            m = sPattern1Line1.matcher(p);
            if (m.matches()) {
                String lineStr = m.group(1);
                String msg = m.group(2);

                // get the matcher for the next line.
                m = getNextLineMatcher(results, ++i, sPattern1Line2);
                if (m == null) {
                    return true;
                }

                String location = m.group(1);

                // check the values and attempt to mark the file.
                if (checkAndMark(location, lineStr, msg, osRoot, project,
                        AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) {
                    return true;
                }
                continue;
            }

            // this needs to be tested before Pattern2 since they both start with 'ERROR:'
            m = sPattern7Line1.matcher(p);
            if (m.matches()) {
                String location = m.group(1);
                String msg = p; // default msg is the line in case we don't find anything else

                if (++i < results.length) {
                    msg = results[i].trim();
                    if (++i < results.length) {
                        msg = msg + " - " + results[i].trim(); //$NON-NLS-1$

                        // skip the next line
                        i++;
                    }
                }

                // display the error
                if (checkAndMark(location, null, msg, osRoot, project,
                        AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) {
                    return true;
                }

                // success, go to the next line
                continue;
            }

            m =  sPattern2Line1.matcher(p);
            if (m.matches()) {
                // get the msg
                String msg = m.group(1);

                // get the matcher for the next line.
                m = getNextLineMatcher(results, ++i, sPattern2Line2);
                if (m == null) {
                    return true;
                }

                String location = m.group(1);
                String lineStr = m.group(2);

                // check the values and attempt to mark the file.
                if (checkAndMark(location, lineStr, msg, osRoot, project,
                        AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) {
                    return true;
                }
                continue;
            }

            m = sPattern3Line1.matcher(p);
            if (m.matches()) {
                String location = m.group(1);
                String lineStr = m.group(2);
                String msg = m.group(3);

                // check the values and attempt to mark the file.
                if (checkAndMark(location, lineStr, msg, osRoot, project,
                        AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) {
                    return true;
                }

                // success, go to the next line
                continue;
            }

            m = sPattern4Line1.matcher(p);
            if (m.matches()) {
                // get the filename.
                String location = m.group(1);

                // get the matcher for the next line.
                m = getNextLineMatcher(results, ++i, sPattern4Line2);
                if (m == null) {
                    return true;
                }

                String msg = m.group(1);
                String lineStr = m.group(2);

                // check the values and attempt to mark the file.
                if (checkAndMark(location, lineStr, msg, osRoot, project,
                        AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) {
                    return true;
                }

                // success, go to the next line
                continue;
            }

            m = sPattern5Line1.matcher(p);
            if (m.matches()) {
                String location = m.group(1);
                String lineStr = m.group(2);
                String msg = m.group(3);

                // check the values and attempt to mark the file.
                if (checkAndMark(location, lineStr, msg, osRoot, project,
                        AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_WARNING) == false) {
                    return true;
                }

                // success, go to the next line
                continue;
            }

            m = sPattern6Line1.matcher(p);
            if (m.matches()) {
                String location = m.group(1);
                String lineStr = m.group(2);
                String msg = m.group(3);

                // check the values and attempt to mark the file.
                if (checkAndMark(location, lineStr, msg, osRoot, project,
                        AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) {
                    return true;
                }

                // success, go to the next line
                continue;
            }

            m = sPattern8Line1.matcher(p);
            if (m.matches()) {
                String location = m.group(2);
                String msg = m.group(1);

                // check the values and attempt to mark the file.
                if (checkAndMark(location, null, msg, osRoot, project,
                        AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) {
                    return true;
                }

                // success, go to the next line
                continue;
            }

            m = sPattern9Line1.matcher(p);
            if (m.matches()) {
                String badConfig = m.group(1);
                String msg = String.format("APK Configuration filter '%1$s' is invalid", badConfig);

                // skip the next line
                i++;

                // check the values and attempt to mark the file.
                if (checkAndMark(null /*location*/, null, msg, osRoot, project,
                        AdtConstants.MARKER_AAPT_PACKAGE, IMarker.SEVERITY_ERROR) == false) {
                    return true;
                }

                // success, go to the next line
                continue;
            }

            m = sSkippingPattern.matcher(p);
            if (m.matches()) {
                String location = m.group(1);

                // check the values and attempt to mark the file.
                if (checkAndMark(location, null, p.trim(), osRoot, project,
                        AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) {
                    return true;
                }

                // success, go to the next line
                continue;
            }

            // invalid line format, flag as error, and bail
            return true;
        }

        return false;
    }

    /**
     * Check if the parameters gotten from the error output are valid, and mark
     * the file with an AAPT marker.
     * @param location the full OS path of the error file. If null, the project is marked
     * @param lineStr
     * @param message
     * @param root The root directory of the project, in OS specific format.
     * @param project
     * @param markerId The marker id to put.
     * @param severity The severity of the marker to put (IMarker.SEVERITY_*)
     * @return true if the parameters were valid and the file was marked successfully.
     *
     * @see IMarker
     */
    private static final  boolean checkAndMark(String location, String lineStr,
            String message, String root, IProject project, String markerId, int severity) {
        // check this is in fact a file
        if (location != null) {
            File f = new File(location);
            if (f.exists() == false) {
                return false;
            }
        }

        // get the line number
        int line = -1; // default value for error with no line.

        if (lineStr != null) {
            try {
                line = Integer.parseInt(lineStr);
            } catch (NumberFormatException e) {
                // looks like the string we extracted wasn't a valid
                // file number. Parsing failed and we return true
                return false;
            }
        }

        // add the marker
        IResource f2 = project;
        if (location != null) {
            f2 = getResourceFromFullPath(location, root, project);
            if (f2 == null) {
                return false;
            }
        }

        // Attempt to determine the exact range of characters affected by this error.
        // This will look up the actual text of the file, go to the particular error line
        // and scan for the specific string mentioned in the error.
        int startOffset = -1;
        int endOffset = -1;
        if (f2 instanceof IFile) {
            IRegion region = findRange((IFile) f2, line, message);
            if (region != null) {
                startOffset = region.getOffset();
                endOffset = startOffset + region.getLength();
            }
        }

        // check if there's a similar marker already, since aapt is launched twice
        boolean markerAlreadyExists = false;
        try {
            IMarker[] markers = f2.findMarkers(markerId, true, IResource.DEPTH_ZERO);

            for (IMarker marker : markers) {
                if (startOffset != -1) {
                    int tmpBegin = marker.getAttribute(IMarker.CHAR_START, -1);
                    if (tmpBegin != startOffset) {
                        break;
                    }
                    int tmpEnd = marker.getAttribute(IMarker.CHAR_END, -1);
                    if (tmpEnd != startOffset) {
                        break;
                    }
                }

                int tmpLine = marker.getAttribute(IMarker.LINE_NUMBER, -1);
                if (tmpLine != line) {
                    break;
                }

                int tmpSeverity = marker.getAttribute(IMarker.SEVERITY, -1);
                if (tmpSeverity != severity) {
                    break;
                }

                String tmpMsg = marker.getAttribute(IMarker.MESSAGE, null);
                if (tmpMsg == null || tmpMsg.equals(message) == false) {
                    break;
                }

                // if we're here, all the marker attributes are equals, we found it
                // and exit
                markerAlreadyExists = true;
                break;
            }

        } catch (CoreException e) {
            // if we couldn't get the markers, then we just mark the file again
            // (since markerAlreadyExists is initialized to false, we do nothing)
        }

        if (markerAlreadyExists == false) {
            BaseProjectHelper.markResource(f2, markerId, message, line,
                    startOffset, endOffset, severity);
        }

        return true;
    }

    /**
     * Given an aapt error message in a given file and a given (initial) line number,
     * return the corresponding offset range for the error, or null.
     */
    private static IRegion findRange(IFile file, int line, String message) {
        Matcher matcher = sValueRangePattern.matcher(message);
        if (matcher.find()) {
            String property = matcher.group(1);
            String value = matcher.group(2);

            // First find the property. We can't just immediately look for the
            // value, because there could be other attributes in this element
            // earlier than the one in error, and we might accidentally pick
            // up on a different occurrence of the value in a context where
            // it is valid.
            if (value.length() > 0) {
                return findRange(file, line, property, value);
            } else {
                // Find first occurrence of property followed by '' or ""
                IRegion region1 = findRange(file, line, property, "\"\""); //$NON-NLS-1$
                IRegion region2 = findRange(file, line, property, "''");   //$NON-NLS-1$
                if (region1 == null) {
                    if (region2 == null) {
                        // Highlight the property instead
                        return findRange(file, line, property, null);
                    }
                    return region2;
                } else if (region2 == null) {
                    return region1;
                } else if (region1.getOffset() < region2.getOffset()) {
                    return region1;
                } else {
                    return region2;
                }
            }
        }

        matcher = sRepeatedRangePattern.matcher(message);
        if (matcher.find()) {
            String property = matcher.group(2);
            return findRange(file, line, property, null);
        }

        matcher = sNoResourcePattern.matcher(message);
        if (matcher.find()) {
            String property = matcher.group(1);
            return findRange(file, line, property, null);
        }

        matcher = sRequiredPattern.matcher(message);
        if (matcher.find()) {
            String elementName = matcher.group(2);
            IRegion region = findRange(file, line, '<' + elementName, null);
            if (region != null && region.getLength() > 1) {
                // Skip the opening <
                region = new Region(region.getOffset() + 1, region.getLength() - 1);
            }
            return region;
        }

        if (message.endsWith(ORIGINALLY_DEFINED_MSG)) {
            return findLineTextRange(file, line);
        }

        return null;
    }

    /**
     * Given a file and line number, return the range of the first match starting on the
     * given line. If second is non null, also search for the second string starting at he
     * location of the first string.
     */
    private static IRegion findRange(IFile file, int line, String first,
            String second) {
        IRegion region = null;
        IDocumentProvider provider = new TextFileDocumentProvider();
        try {
            provider.connect(file);
            IDocument document = provider.getDocument(file);
            if (document != null) {
                IRegion lineInfo = document.getLineInformation(line - 1);
                int lineStartOffset = lineInfo.getOffset();
                // The aapt errors will be anchored on the line where the
                // element starts - which means that with formatting where
                // attributes end up on subsequent lines we don't find it on
                // the error line indicated by aapt.
                // Therefore, search forwards in the document.
                FindReplaceDocumentAdapter adapter =
                    new FindReplaceDocumentAdapter(document);

                region = adapter.find(lineStartOffset, first,
                        true /*forwardSearch*/, true /*caseSensitive*/,
                        false /*wholeWord*/, false /*regExSearch*/);
                if (region != null && second != null) {
                    region = adapter.find(region.getOffset() + first.length(), second,
                            true /*forwardSearch*/, true /*caseSensitive*/,
                            false /*wholeWord*/, false /*regExSearch*/);
                }
            }
        } catch (Exception e) {
            AdtPlugin.log(e, "Can't find range information for %1$s", file.getName());
        } finally {
            provider.disconnect(file);
        }
        return region;
    }

    /** Returns the non-whitespace line range at the given line number. */
    private static IRegion findLineTextRange(IFile file, int line) {
        IDocumentProvider provider = new TextFileDocumentProvider();
        try {
            provider.connect(file);
            IDocument document = provider.getDocument(file);
            if (document != null) {
                IRegion lineInfo = document.getLineInformation(line - 1);
                String lineContents = document.get(lineInfo.getOffset(), lineInfo.getLength());
                int lineBegin = 0;
                int lineEnd = lineContents.length()-1;

                for (; lineEnd >= 0; lineEnd--) {
                    char c = lineContents.charAt(lineEnd);
                    if (!Character.isWhitespace(c)) {
                        break;
                    }
                }
                lineEnd++;
                for (; lineBegin < lineEnd; lineBegin++) {
                    char c = lineContents.charAt(lineBegin);
                    if (!Character.isWhitespace(c)) {
                        break;
                    }
                }
                if (lineBegin < lineEnd) {
                    return new Region(lineInfo.getOffset() + lineBegin, lineEnd - lineBegin);
                }
            }
        } catch (Exception e) {
            AdtPlugin.log(e, "Can't find range information for %1$s", file.getName());
        } finally {
            provider.disconnect(file);
        }

        return null;
    }

    /**
     * Returns a matching matcher for the next line
     * @param lines The array of lines
     * @param nextIndex The index of the next line
     * @param pattern The pattern to match
     * @return null if error or no match, the matcher otherwise.
     */
    private static final Matcher getNextLineMatcher(String[] lines,
            int nextIndex, Pattern pattern) {
        // unless we can't, because we reached the last line
        if (nextIndex == lines.length) {
            // we expected a 2nd line, so we flag as error
            // and we bail
            return null;
        }

        Matcher m = pattern.matcher(lines[nextIndex]);
        if (m.matches()) {
           return m;
        }

        return null;
    }

    private static IResource getResourceFromFullPath(String filename, String root,
            IProject project) {
        if (filename.startsWith(root)) {
            String file = filename.substring(root.length());

            // get the resource
            IResource r = project.findMember(file);

            // if the resource is valid, we add the marker
            if (r.exists()) {
                return r;
            }
        }

        return null;
    }

}
