/*
 * 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.
 */
package com.android.ide.eclipse.adt.internal.editors;

import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_EMPTY_TAG_CLOSE;
import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_END_TAG_OPEN;
import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_TAG_CLOSE;
import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_TAG_NAME;
import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_TAG_OPEN;

import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList;
import org.eclipse.wst.xml.ui.internal.text.XMLDocumentRegionEdgeMatcher;

/**
 * Custom version of the character matcher for XML files which adds the ability to
 * jump between open and close tags in the XML file.
 */
@SuppressWarnings("restriction")
public class AndroidXmlCharacterMatcher extends XMLDocumentRegionEdgeMatcher {
    /**
     * Constructs a new character matcher for Android XML files
     */
    public AndroidXmlCharacterMatcher() {
    }

    @Override
    public IRegion match(IDocument doc, int offset) {
        if (offset < 0 || offset >= doc.getLength()) {
            return null;
        }

        IRegion match = findOppositeTag(doc, offset);
        if (match != null) {
            return match;
        }

        return super.match(doc, offset);
    }

    private IRegion findOppositeTag(IDocument document, int offset) {
        if (!(document instanceof IStructuredDocument)) {
            return null;
        }
        IStructuredDocument doc = (IStructuredDocument) document;

        IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(offset);
        if (region == null) {
            return null;
        }

        ITextRegion subRegion = region.getRegionAtCharacterOffset(offset);
        if (subRegion == null) {
            return null;
        }
        ITextRegionList subRegions = region.getRegions();
        int index = subRegions.indexOf(subRegion);

        String type = subRegion.getType();
        boolean isOpenTag = false;
        boolean isCloseTag = false;

        if (type.equals(XML_TAG_OPEN)) {
            isOpenTag = true;
        } else if (type.equals(XML_END_TAG_OPEN)) {
            isCloseTag = true;
        } else if (!(type.equals(XML_TAG_CLOSE) || type.equals(XML_TAG_NAME)) &&
                (subRegion.getStart() + region.getStartOffset() == offset)) {
            // Look to the left one character; we may have the case where you're
            // pointing to the right of a tag, e.g.
            //     <foo>^text
            offset--;
            region = doc.getRegionAtCharacterOffset(offset);
            if (region == null) {
                return null;
            }
            subRegion = region.getRegionAtCharacterOffset(offset);
            if (subRegion == null) {
                return null;
            }
            type = subRegion.getType();

            subRegions = region.getRegions();
            index = subRegions.indexOf(subRegion);
        }

        if (type.equals(XML_TAG_CLOSE) || type.equals(XML_TAG_NAME)) {
            for (int i = index; i >= 0; i--) {
                subRegion = subRegions.get(i);
                type = subRegion.getType();
                if (type.equals(XML_TAG_OPEN)) {
                    isOpenTag = true;
                    break;
                } else if (type.equals(XML_END_TAG_OPEN)) {
                    isCloseTag = true;
                    break;
                }
            }
        }

        if (isOpenTag) {
            // Find closing tag
            int target = findTagForwards(doc, subRegion.getStart() + region.getStartOffset(), 0);
            // Note - there is no point in looking up the whole region for the matching
            // tag, because even if you pass a length greater than 1 here, the paint highlighter
            // will only highlight a single character -- the *last* character of the region,
            // not the whole region itself.
            return new Region(target, 1);
        } else if (isCloseTag) {
            // Find open tag
            int target = findTagBackwards(doc, subRegion.getStart() + region.getStartOffset(), -1);
            return new Region(target, 1);
        }

        return null;
    }

    /**
     * Finds the corresponding open tag by searching backwards until the tag balance
     * reaches a given target.
     *
     * @param doc the document
     * @param offset the ending offset (where the search begins searching backwards from)
     * @param targetTagBalance the balance to end the search at
     * @return the offset of the beginning of the open tag
     */
    public static int findTagBackwards(IStructuredDocument doc, int offset, int targetTagBalance) {
        // Balance of open and closing tags
        int tagBalance = 0;
        // Balance of open and closing brackets
        IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(offset);
        if (region != null) {
            boolean inEmptyTag = true;

            while (region != null) {
                int regionStart = region.getStartOffset();
                ITextRegionList subRegions = region.getRegions();
                for (int i = subRegions.size() - 1; i >= 0; i--) {
                    ITextRegion subRegion = subRegions.get(i);
                    int subRegionStart = regionStart + subRegion.getStart();
                    if (subRegionStart >= offset) {
                        continue;
                    }
                    String type = subRegion.getType();

                    // Iterate backwards and keep track of the tag balance such that
                    // we can find the corresponding opening tag

                    if (XML_TAG_OPEN.equals(type)) {
                        if (!inEmptyTag) {
                            tagBalance--;
                        }
                        if (tagBalance == targetTagBalance) {
                            return subRegionStart;
                        }
                    } else if (XML_END_TAG_OPEN.equals(type)) {
                        tagBalance++;
                    } else if (XML_EMPTY_TAG_CLOSE.equals(type)) {
                        inEmptyTag = true;
                    } else if (XML_TAG_CLOSE.equals(type)) {
                        inEmptyTag = false;
                    }
                }

                region = region.getPrevious();
            }
        }

        return -1;
    }

    /**
     * Finds the corresponding closing tag by searching forwards until the tag balance
     * reaches a given target.
     *
     * @param doc the document
     * @param start the starting offset (where the search begins searching forwards from)
     * @param targetTagBalance the balance to end the search at
     * @return the offset of the beginning of the closing tag
     */
    public static int findTagForwards(IStructuredDocument doc, int start, int targetTagBalance) {
        int tagBalance = 0;
        IStructuredDocumentRegion region = doc.getRegionAtCharacterOffset(start);

        if (region != null) {
            while (region != null) {
                int regionStart = region.getStartOffset();
                ITextRegionList subRegions = region.getRegions();
                for (int i = 0, n = subRegions.size(); i < n; i++) {
                    ITextRegion subRegion = subRegions.get(i);
                    int subRegionStart = regionStart + subRegion.getStart();
                    int subRegionEnd = regionStart + subRegion.getEnd();
                    if (subRegionEnd < start) {
                        continue;
                    }
                    String type = subRegion.getType();

                    if (XML_TAG_OPEN.equals(type)) {
                        tagBalance++;
                    } else if (XML_END_TAG_OPEN.equals(type)) {
                        tagBalance--;
                        if (tagBalance == targetTagBalance) {
                            return subRegionStart;
                        }
                    } else if (XML_EMPTY_TAG_CLOSE.equals(type)) {
                        tagBalance--;
                        if (tagBalance == targetTagBalance) {
                            // We don't jump to matching tags within a self-closed tag
                            return -1;
                        }
                    }
                }

                region = region.getNext();
            }
        }

        return -1;
    }
}
