package page.tools.xml;

import java.text.ParseException;
import java.util.Map;
import java.util.HashMap;

public abstract class WikiParser {
    
    public abstract void openTag(String tagName, Map<String, String> attributes);
    
    public abstract void closeTag(String tagName);
    
    public String getTextAfterTag() {
        return text;
    }    
    
    public void parse(String page) throws ParseException {
        
        int idx = 0;
        int tagStartIdx = nextTagIdx(page, idx);
        int tagEndIdx = -1;
        Map<String, String> attributes = new HashMap<String, String>();
        
        while (tagStartIdx > -1) {
            
            attributes.clear();            
            boolean closingTag = isClosingTag(tagStartIdx, page);
            String tagName = getTagName(tagStartIdx, page).toLowerCase();
            int tagNameEndIdx = getTagNameEndIdx(tagStartIdx, page);  
            
            // If comment, silently ignore
            if (tagName.startsWith("!--")) {
                tagEndIdx = page.indexOf("-->", tagNameEndIdx);
                if (tagEndIdx == -1) {
                    throw new ParseException("Incomplete comment", tagStartIdx);
                }                
                tagEndIdx += 3;
            }
            
            // Parse tag and properties
            else {
                tagEndIdx = parseTagAttributes(tagNameEndIdx, page, attributes);
            }
            
            // Look ahead for next tag
            tagStartIdx = nextTagIdx(page, tagEndIdx);
            
            // Store the text after tag
            if (tagEndIdx > -1 && tagEndIdx < tagStartIdx) {
                setText(page.substring(tagEndIdx, tagStartIdx));
            } else setText(null);
            
            // Notify tag listeners
            if (closingTag) closeTag(tagName);
            else openTag(tagName, attributes);            
                        
        }
    } 
    
    
    
    
    private static int nextTagIdx(String page, int startIdx) throws ParseException {
        return page.indexOf('<', startIdx);  
    }
    
    private static int parseTagAttributes(int attributeStartIdx, String page, Map<String,String> attributes) throws ParseException {
        if (page.charAt(attributeStartIdx) == '>') return attributeStartIdx+1;
        
        while (!Thread.currentThread().isInterrupted()) {
            while (page.charAt(attributeStartIdx) == ' ') {
                attributeStartIdx++;
                if (attributeStartIdx == page.length()) {
                    throw new ParseException("Tag incomplete", attributeStartIdx);
                }
            }
            
            // Locate key end
            int keyStartIdx = attributeStartIdx;
            int keyEnd1Idx = page.indexOf("=", attributeStartIdx);
            int keyEnd2Idx = page.indexOf(" ", attributeStartIdx);
            int keyEnd3Idx = page.indexOf(">", attributeStartIdx);
            if (keyEnd1Idx == -1 && keyEnd2Idx == -1 && keyEnd3Idx == -1) {
                throw new ParseException("Tag incomplete", attributeStartIdx);
            }           
            if (keyStartIdx == keyEnd3Idx) {
                return keyEnd3Idx+1;
            }        
            int keyEndIdx = minValid(keyEnd1Idx, keyEnd2Idx, keyEnd3Idx);
            String key = page.substring(keyStartIdx, keyEndIdx);
            if (keyEndIdx == keyEnd3Idx) {
                attributes.put(key, "true");
                return keyEndIdx+1;
            }
            if (keyEndIdx == keyEnd2Idx) {
                attributes.put(key, "true");
                attributeStartIdx = keyEndIdx;
            }   
            else {
                // Locate value end
                char quot = page.charAt(keyEndIdx+1);
                if (quot == '\'' || quot == '\"') {
                    int valueStartIdx = keyEndIdx+2;
                    int valueEndIdx = page.indexOf(quot, valueStartIdx);
                    if (valueEndIdx == -1) {
                        throw new ParseException("Unclosed value in tag", attributeStartIdx);
                    }             
                    String value = page.substring(valueStartIdx, valueEndIdx);
                    attributes.put(key, value);
                    attributeStartIdx = valueEndIdx+1;
                } 
                else {
                    int valueStartIdx = keyEndIdx+1;
                    int valueEnd1Idx = page.indexOf(" ", valueStartIdx);
                    int valueEnd2Idx = page.indexOf(">", valueStartIdx); 
                    if (valueEnd1Idx == -1 && valueEnd2Idx == -1) {
                        throw new ParseException("Value in tag incomplete", attributeStartIdx);
                    }    
                    int valueEndIdx = minValid(valueEnd1Idx, valueEnd2Idx);
                    String value = page.substring(valueStartIdx, valueEndIdx);
                    attributes.put(key, value);
                    if (keyEndIdx == keyEnd2Idx) return keyEndIdx;    
                    attributeStartIdx = valueEndIdx;                
                }
            }
        }
        
        throw new ParseException("Tag parsing interrupted", attributeStartIdx);
    }
    
    private static int getTagNameEndIdx(int tagStartIdx, String page) throws ParseException {
        tagStartIdx++;
        if (tagStartIdx >= page.length()) {
            throw new ParseException("Tag incomplete", tagStartIdx);
        }        
        if (page.charAt(tagStartIdx) == '/') tagStartIdx++;
        int tagEnd1Idx = page.indexOf('>', tagStartIdx); 
        int tagEnd2Idx = page.indexOf(' ', tagStartIdx);
        int tagEnd3Idx = page.indexOf('/', tagStartIdx); 
        if (tagEnd1Idx == -1 && tagEnd2Idx == -1 && tagEnd3Idx == -1) {
            throw new ParseException("Tag incomplete", tagStartIdx);
        }        
        return minValid(tagEnd1Idx, tagEnd2Idx, tagEnd3Idx);
    }
    
    private static String getTagName(int tagStartIdx, String page) throws ParseException {
        if (page.charAt(tagStartIdx) != '<') {
            throw new ParseException("Tag expected", tagStartIdx);
        }     
        int tagNameEndIdx = getTagNameEndIdx(tagStartIdx, page);
        
        tagStartIdx++;
        if (page.charAt(tagStartIdx) == '/') tagStartIdx++;
        return page.substring(tagStartIdx, tagNameEndIdx);
        
    }
    
    private static boolean isClosingTag(int tagStartIdx, String page) throws ParseException {
        if (tagStartIdx == -1) {
            throw new ParseException("tagStartIdx invalid", tagStartIdx);
        }
        if (tagStartIdx > page.length()-1) {
            throw new ParseException("tagStartIdx exceeds page length", tagStartIdx);
        }         
        if (tagStartIdx == page.length()-1) return false;
        
        return page.charAt(tagStartIdx+1) == '/';
    }
    
    private void setText(String text) {
        this.text = text;
    }     
    
    private static int minValid(int i, int j, int k) {
        return minValid(minValid(i, j), minValid(j, k));
    }    
    
    private static int minValid(int a, int b) {
        if (a == -1) return b;
        if (b == -1) return a;
        return (a < b) ? a : b;
    }
    
    private String text;
}

