/*
 * XModeHandler.java - XML handler for mode files
 * :tabSize=4:indentSize=4:noTabs=false:
 * :folding=explicit:collapseFolds=1:
 *
 * Copyright (C) 1999 mike dillon
 * Portions copyright (C) 2000, 2001 Slava Pestov
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

package org.gjt.sp.jedit.syntax;

//{{{ Imports
import java.util.*;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.helpers.DefaultHandler;

import org.gjt.sp.jedit.Mode;
import org.gjt.sp.util.Log;
import org.gjt.sp.util.XMLUtilities;
//}}}

/**
 * XML handler for mode definition files.
 * @version $Id: XModeHandler.java 25489 2021-04-05 21:18:30Z kpouer $
 */
public abstract class XModeHandler extends DefaultHandler
{
	//{{{ XModeHandler constructor
	protected XModeHandler(String modeName)
	{
		this.modeName = modeName;
		marker = new TokenMarker();
		marker.addRuleSet(new ParserRuleSet(modeName,"MAIN"));
		stateStack = new Stack<>();
	} //}}}

	//{{{ resolveEntity() method
	@Override
	public InputSource resolveEntity(String publicId, String systemId)
	{
		return XMLUtilities.findEntity(systemId, "xmode.dtd", XModeHandler.class);
	} //}}}

	//{{{ characters() method
	@Override
	public void characters(char[] c, int off, int len)
	{
		peekElement().setText(c, off, len);
	} //}}}

	//{{{ startElement() method
	@Override
	public void startElement(String uri, String localName,
				 String qName, Attributes attrs)
	{
		TagDecl tag = pushElement(qName, attrs);

		if ("WHITESPACE".equals(qName))
		{
			Log.log(Log.WARNING,this,modeName + ": WHITESPACE rule "
				+ "no longer needed");
		}
		else if ("KEYWORDS".equals(qName))
		{
			keywords = new KeywordMap(rules.getIgnoreCase());
		}
		else if ("RULES".equals(qName))
		{
			assert tag != null;
			// if qName is not null then pushElement will not returns null
			if(tag.lastSetName == null)
				tag.lastSetName = "MAIN";
			rules = marker.getRuleSet(tag.lastSetName);
			if(rules == null)
			{
				rules = new ParserRuleSet(modeName,tag.lastSetName);
				marker.addRuleSet(rules);
			}
			rules.setIgnoreCase(tag.lastIgnoreCase);
			rules.setHighlightDigits(tag.lastHighlightDigits);
			if(tag.lastDigitRE != null)
			{
				try
				{
					rules.setDigitRegexp(Pattern.compile(tag.lastDigitRE,
						tag.lastIgnoreCase
						? Pattern.CASE_INSENSITIVE : 0));
				}
				catch(PatternSyntaxException e)
				{
					error("regexp",e);
				}
			}

			if(tag.lastEscape != null)
				rules.setEscapeRule(ParserRule.createEscapeRule(tag.lastEscape));
			rules.setDefault(tag.lastDefaultID);
			rules.setNoWordSep(tag.lastNoWordSep);
		}
	} //}}}

	//{{{ endElement() method
	@Override
	public void endElement(String uri, String localName, String name)
	{
		TagDecl tag = popElement();
		if (name.equals(tag.tagName))
		{
			if(tag.lastDelegateSet != null
					&& ! tag.tagName.equals("IMPORT")
					&& ! tag.lastDelegateSet.getModeName().equals(modeName))
			{
				Mode mode = ModeProvider.instance.getMode(tag.lastDelegateSet.getModeName());
				if( ! reloadModes.contains(mode) )
				{
					reloadModes.add(mode);
				}
			}
			//{{{ PROPERTY
			if (tag.tagName.equals("PROPERTY"))
			{
				props.put(propName,propValue);
			} //}}}
			//{{{ PROPS
			else if (tag.tagName.equals("PROPS"))
			{
				if(peekElement().tagName.equals("RULES"))
					rules.setProperties(props);
				else
					modeProps = props;

				props = new Hashtable<>();
			} //}}}
			//{{{ RULES
			else if (tag.tagName.equals("RULES"))
			{
				rules.setKeywords(keywords);
				keywords = null;
				rules = null;
			} //}}}
			//{{{ IMPORT
			else if (tag.tagName.equals("IMPORT"))
			{
				// prevent lockups
				if (!rules.equals(tag.lastDelegateSet))
				{
					rules.addRuleSet(tag.lastDelegateSet);
				}
			} //}}}
			//{{{ TERMINATE
			else if (tag.tagName.equals("TERMINATE"))
			{
				rules.setTerminateChar(tag.termChar);
			} //}}}
			//{{{ SEQ
			else if (tag.tagName.equals("SEQ"))
			{
				if(tag.lastStart == null)
				{
					error("empty-tag","SEQ");
					return;
				}

				rules.addRule(ParserRule.createSequenceRule(
					tag.lastStartPosMatch,tag.lastStart.toString(),
					tag.lastDelegateSet,tag.lastTokenID));
			} //}}}
			//{{{ SEQ_REGEXP
			else if (tag.tagName.equals("SEQ_REGEXP"))
			{
				if(tag.lastStart == null)
				{
					error("empty-tag","SEQ_REGEXP");
					return;
				}

				try
				{
					if (null != tag.lastHashChars)
					{
						rules.addRule(ParserRule.createRegexpSequenceRule(
							tag.lastStartPosMatch,tag.lastHashChars.toCharArray(),
							tag.lastStart.toString(),tag.lastDelegateSet,
							tag.lastTokenID,findParent("RULES").lastIgnoreCase));
					}
					else
					{
						rules.addRule(ParserRule.createRegexpSequenceRule(
							tag.lastHashChar,tag.lastStartPosMatch,
							tag.lastStart.toString(),tag.lastDelegateSet,
							tag.lastTokenID,findParent("RULES").lastIgnoreCase));
					}
				}
				catch(PatternSyntaxException re)
				{
					error("regexp",re);
				}
			} //}}}
			//{{{ SPAN
			else if (tag.tagName.equals("SPAN"))
			{
				if(tag.lastStart == null)
				{
					error("empty-tag","BEGIN");
					return;
				}

				if(tag.lastEnd == null)
				{
					error("empty-tag","END");
					return;
				}

				rules.addRule(ParserRule
					.createSpanRule(
					tag.lastStartPosMatch,tag.lastStart.toString(),
					tag.lastEndPosMatch,tag.lastEnd.toString(),
					tag.lastDelegateSet,
					tag.lastTokenID,tag.lastMatchType,
					tag.lastNoLineBreak,
					tag.lastNoWordBreak,
					tag.lastEscape));
			} //}}}
			//{{{ SPAN_REGEXP
			else if (tag.tagName.equals("SPAN_REGEXP"))
			{
				if(tag.lastStart == null)
				{
					error("empty-tag","BEGIN");
					return;
				}

				if(tag.lastEnd == null)
				{
					error("empty-tag","END");
					return;
				}

				try
				{
					if (null != tag.lastHashChars)
					{
						rules.addRule(ParserRule
							.createRegexpSpanRule(
							tag.lastStartPosMatch,tag.lastHashChars.toCharArray(),
							tag.lastStart.toString(),
							tag.lastEndPosMatch,tag.lastEnd.toString(),
							tag.lastDelegateSet,
							tag.lastTokenID,
							tag.lastMatchType,
							tag.lastNoLineBreak,
							tag.lastNoWordBreak,
							findParent("RULES").lastIgnoreCase,
							tag.lastEscape,
							tag.lastEndRegexp));
					}
					else
					{
						rules.addRule(ParserRule
							.createRegexpSpanRule(
							tag.lastHashChar,
							tag.lastStartPosMatch,tag.lastStart.toString(),
							tag.lastEndPosMatch,tag.lastEnd.toString(),
							tag.lastDelegateSet,
							tag.lastTokenID,
							tag.lastMatchType,
							tag.lastNoLineBreak,
							tag.lastNoWordBreak,
							findParent("RULES").lastIgnoreCase,
							tag.lastEscape,
							tag.lastEndRegexp));
					}
				}
				catch(PatternSyntaxException re)
				{
					error("regexp",re);
				}
			} //}}}
			//{{{ EOL_SPAN
			else if (tag.tagName.equals("EOL_SPAN"))
			{
				if(tag.lastStart == null)
				{
					error("empty-tag","EOL_SPAN");
					return;
				}

				rules.addRule(ParserRule.createEOLSpanRule(
					tag.lastStartPosMatch,tag.lastStart.toString(),
					tag.lastDelegateSet,tag.lastTokenID,
					tag.lastMatchType));
			} //}}}
			//{{{ EOL_SPAN_REGEXP
			else if (tag.tagName.equals("EOL_SPAN_REGEXP"))
			{
				if(tag.lastStart == null)
				{
					error("empty-tag","EOL_SPAN_REGEXP");
					return;
				}

				try
				{
					if (null != tag.lastHashChars)
					{
						rules.addRule(ParserRule.createRegexpEOLSpanRule(
							tag.lastStartPosMatch,tag.lastHashChars.toCharArray(),
							tag.lastStart.toString(),tag.lastDelegateSet,
							tag.lastTokenID,tag.lastMatchType,
							findParent("RULES").lastIgnoreCase));
					}
					else
					{
						rules.addRule(ParserRule.createRegexpEOLSpanRule(
							tag.lastHashChar,tag.lastStartPosMatch,
							tag.lastStart.toString(),tag.lastDelegateSet,
							tag.lastTokenID,tag.lastMatchType,
							findParent("RULES").lastIgnoreCase));
					}
				}
				catch(PatternSyntaxException re)
				{
					error("regexp",re);
				}
			} //}}}
			//{{{ MARK_FOLLOWING
			else if (tag.tagName.equals("MARK_FOLLOWING"))
			{
				if(tag.lastStart == null)
				{
					error("empty-tag","MARK_FOLLOWING");
					return;
				}

				rules.addRule(ParserRule
					.createMarkFollowingRule(
					tag.lastStartPosMatch,tag.lastStart.toString(),
					tag.lastTokenID,tag.lastMatchType));
			} //}}}
			//{{{ MARK_PREVIOUS
			else if (tag.tagName.equals("MARK_PREVIOUS"))
			{
				if(tag.lastStart == null)
				{
					error("empty-tag","MARK_PREVIOUS");
					return;
				}

				rules.addRule(ParserRule
					.createMarkPreviousRule(
					tag.lastStartPosMatch,tag.lastStart.toString(),
					tag.lastTokenID,tag.lastMatchType));
			} //}}}
			//{{{ Keywords
			else if (
				!tag.tagName.equals("END")
				&& !tag.tagName.equals("BEGIN")
				&& !tag.tagName.equals("KEYWORDS")
				&& !tag.tagName.equals("MODE"))
			{
				byte token = Token.stringToToken(tag.tagName);
				if(token != -1)
				{
					if (tag.lastKeyword == null
						|| tag.lastKeyword.length() == 0)
					{
						error("empty-keyword", null);
					}
					else
					{
						addKeyword(tag.lastKeyword.toString(),token);
					}
				}
			} //}}}
		}
		else
		{
			// can't happen
			throw new InternalError();
		}
	} //}}}

	//{{{ startDocument() method
	@Override
	public void startDocument()
	{
		props = new Hashtable<>();
		pushElement(null, null);
		reloadModes = new Vector<>();
	} //}}}

	//{{{ endDocument() method
	@Override
	public void endDocument()
	{
		ParserRuleSet[] rulesets = marker.getRuleSets();
		for(int i = 0; i < rulesets.length; i++)
		{
			rulesets[i].resolveImports();
		}
		for(Mode mode : reloadModes)
		{
			mode.setTokenMarker(null);
			mode.loadIfNecessary();
		}
	} //}}}

	//{{{ getTokenMarker() method
	/**
	 * Returns the TokenMarker.
	 *
	 * @return a TokenMarker it cannot be null
	 */
	public TokenMarker getTokenMarker()
	{
		return marker;
	} //}}}

	//{{{ getModeProperties() method
	public Hashtable<String, String> getModeProperties()
	{
		return modeProps;
	} //}}}

	//{{{ Protected members

	//{{{ error() method
	/**
	 * Reports an error.
	 * You must override this method so that the mode loader can do error
	 * reporting.
	 * @param msg The error type
	 * @param subst A <code>String</code> or a <code>Throwable</code>
	 * containing specific information
	 * @since jEdit 4.2pre1
	 */
	protected abstract void error(String msg, Object subst);
	//}}}

	//{{{ getTokenMarker() method
	/**
	 * Returns the token marker for the given mode.
	 * You must override this method so that the mode loader can resolve
	 * delegate targets.
	 * @param mode The mode name
	 * @since jEdit 4.2pre1
	 */
	protected abstract TokenMarker getTokenMarker(String mode);
	//}}}

	//}}}

	//{{{ Private members

	//{{{ Instance variables
	private final String modeName;
	/** The token marker cannot be null. */
	private final TokenMarker marker;
	private KeywordMap keywords;
	/** this stack can contains null elements. */
	private final Stack<TagDecl> stateStack;
	private String propName;
	private String propValue;
	private Hashtable<String, String> props;
	private Hashtable<String, String> modeProps;
	private ParserRuleSet rules;
	/**
	 *  A list of modes to be reloaded at the end, loaded through DELEGATEs
	 *  @see http://sourceforge.net/tracker/index.php?func=detail&aid=1742250&group_id=588&atid=100588
	 */
	private Vector<Mode> reloadModes;
	//}}}

	//{{{ addKeyword() method
	private void addKeyword(String k, byte id)
	{
		if (keywords == null) return;
		keywords.add(k,id);
	} //}}}

	//{{{ pushElement() method
	private TagDecl pushElement(String name, Attributes attrs)
	{
		if (name != null)
		{
			TagDecl tag = new TagDecl(name, attrs);
			stateStack.push(tag);
			return tag;
		}
		else
		{
			stateStack.push(null);
			return null;
		}
	} //}}}

	//{{{ peekElement() method
	private TagDecl peekElement()
	{
		return stateStack.peek();
	} //}}}

	//{{{ popElement() method
	private TagDecl popElement()
	{
		return stateStack.pop();
	} //}}}

	//{{{ findParent() method
	/**
	 * Finds the first element whose tag matches 'tagName',
	 * searching backwards in the stack.
	 */
	private TagDecl findParent(String tagName)
	{
		for (int idx = stateStack.size() - 1; idx >= 0; idx--)
		{
			TagDecl tag = stateStack.get(idx);
			if (tag.tagName.equals(tagName))
				return tag;
		}
		return null;
	} //}}}

	//}}}

	/**
	 * Hold info about what tag was read and what attributes were
	 * set in the XML file, to be kept by the handler in a stack
	 * (similar to the way tag names were kept in a stack before).
	 */
	private class TagDecl
	{
		TagDecl(String tagName, Attributes attrs)
		{
			this.tagName = tagName;

			String tmp;

			propName = attrs.getValue("NAME");
			propValue = attrs.getValue("VALUE");

			tmp = attrs.getValue("TYPE");
			if (tmp != null)
			{
				lastTokenID = Token.stringToToken(tmp);
				if(lastTokenID == -1)
					error("token-invalid",tmp);
			}

			lastMatchType = ParserRule.MATCH_TYPE_RULE;
			// check for the deprecated "EXCLUDE_MATCH" and
			// warn if found.
			tmp = attrs.getValue("EXCLUDE_MATCH");
			if (tmp != null)
			{
				Log.log(Log.WARNING, this, modeName + ": EXCLUDE_MATCH is deprecated");
				if ("TRUE".equalsIgnoreCase(tmp))
				{
					lastMatchType = ParserRule.MATCH_TYPE_CONTEXT;
				}
			}

			// override with the newer MATCH_TYPE if present
			tmp = attrs.getValue("MATCH_TYPE");
			if (tmp != null)
			{
				if ("CONTEXT".equals(tmp))
				{
					lastMatchType = ParserRule.MATCH_TYPE_CONTEXT;
				}
				else if ("RULE".equals(tmp))
				{
					lastMatchType = ParserRule.MATCH_TYPE_RULE;
				}
				else
				{
					lastMatchType = Token.stringToToken(tmp);
					if(lastMatchType == -1)
						error("token-invalid",tmp);
				}
			}

			lastAtLineStart = "TRUE".equals(attrs.getValue("AT_LINE_START"));
			lastAtWhitespaceEnd = "TRUE".equals(attrs.getValue("AT_WHITESPACE_END"));
			lastAtWordStart = "TRUE".equals(attrs.getValue("AT_WORD_START"));
			lastNoLineBreak = "TRUE".equals(attrs.getValue("NO_LINE_BREAK"));
			lastNoWordBreak = "TRUE".equals(attrs.getValue("NO_WORD_BREAK"));
			lastIgnoreCase = (attrs.getValue("IGNORE_CASE") == null ||
					"TRUE".equals(attrs.getValue("IGNORE_CASE")));
			lastHighlightDigits = "TRUE".equals(attrs.getValue("HIGHLIGHT_DIGITS"));
			lastRegexp = "TRUE".equals(attrs.getValue("REGEXP"));
			lastDigitRE = attrs.getValue("DIGIT_RE");

			tmp = attrs.getValue("NO_WORD_SEP");
			if (tmp != null)
				lastNoWordSep = tmp;

			tmp = attrs.getValue("AT_CHAR");
			if (tmp != null)
			{
				try
				{
					termChar = Integer.parseInt(tmp);
				}
				catch (NumberFormatException e)
				{
					error("termchar-invalid",tmp);
					termChar = -1;
				}
			}

			lastEscape = attrs.getValue("ESCAPE");
			lastSetName = attrs.getValue("SET");

			tmp = attrs.getValue("DELEGATE");
			if (tmp != null)
			{
				String delegateMode, delegateSetName;

				int index = tmp.indexOf("::");

				if(index != -1)
				{
					delegateMode = tmp.substring(0,index);
					delegateSetName = tmp.substring(index + 2);
				}
				else
				{
					delegateMode = modeName;
					delegateSetName = tmp;
				}

				TokenMarker delegateMarker = getTokenMarker(delegateMode);
				if(delegateMarker == null)
					error("delegate-invalid",tmp);
				else
				{
					lastDelegateSet = delegateMarker
						.getRuleSet(delegateSetName);
					if(delegateMarker == marker
						&& lastDelegateSet == null)
					{
						// stupid hack to handle referencing
						// a rule set that is defined later!
						lastDelegateSet = new ParserRuleSet(
							delegateMode,
							delegateSetName);
						lastDelegateSet.setDefault(Token.INVALID);
						marker.addRuleSet(lastDelegateSet);
					}
					else if(lastDelegateSet == null)
						error("delegate-invalid",tmp);
				}
			}

			tmp = attrs.getValue("DEFAULT");
			if (tmp != null)
			{
				lastDefaultID = Token.stringToToken(tmp);
				if(lastDefaultID == -1)
				{
					error("token-invalid",tmp);
					lastDefaultID = Token.NULL;
				}
			}

			lastHashChar = attrs.getValue("HASH_CHAR");
			lastHashChars = attrs.getValue("HASH_CHARS");
			if ((null != lastHashChar) && (null != lastHashChars))
			{
				error("hash-char-and-hash-chars-mutually-exclusive",null);
				lastHashChars = null;
			}
		}

		public void setText(char[] c, int off, int len)
		{
			if (tagName.equals("EOL_SPAN") ||
				tagName.equals("EOL_SPAN_REGEXP") ||
				tagName.equals("MARK_PREVIOUS") ||
				tagName.equals("MARK_FOLLOWING") ||
				tagName.equals("SEQ") ||
				tagName.equals("SEQ_REGEXP") ||
				tagName.equals("BEGIN")
			)
			{
				TagDecl target = this;
				if (tagName.equals("BEGIN"))
					target = stateStack.get(stateStack.size() - 2);

				if (target.lastStart == null)
				{
					target.lastStart = new StringBuffer();
					target.lastStart.append(c, off, len);
					target.lastStartPosMatch = ((target.lastAtLineStart ? ParserRule.AT_LINE_START : 0)
						| (target.lastAtWhitespaceEnd ? ParserRule.AT_WHITESPACE_END : 0)
						| (target.lastAtWordStart ? ParserRule.AT_WORD_START : 0));
					target.lastAtLineStart = false;
					target.lastAtWordStart = false;
					target.lastAtWhitespaceEnd = false;
				}
				else
				{
					target.lastStart.append(c, off, len);
				}
			}
			else if (tagName.equals("END"))
			{
				TagDecl target = stateStack.get(stateStack.size() - 2);
				if (target.lastEnd == null)
				{
					target.lastEnd = new StringBuffer();
					target.lastEnd.append(c, off, len);
					target.lastEndPosMatch = ((lastAtLineStart ? ParserRule.AT_LINE_START : 0)
						| (lastAtWhitespaceEnd ? ParserRule.AT_WHITESPACE_END : 0)
						| (lastAtWordStart ? ParserRule.AT_WORD_START : 0));
					target.lastEndRegexp = lastRegexp;
					target.lastAtLineStart = false;
					target.lastAtWordStart = false;
					target.lastAtWhitespaceEnd = false;
				}
				else
				{
					target.lastEnd.append(c, off, len);
				}
			}
			else
			{
				if (lastKeyword == null)
					lastKeyword = new StringBuffer();
				lastKeyword.append(c, off, len);
			}
		}

		public String tagName;
		public StringBuffer lastStart;
		public StringBuffer lastEnd;
		public StringBuffer lastKeyword;
		public String lastSetName;
		public String lastEscape;
		public ParserRuleSet lastDelegateSet;
		public String lastNoWordSep = "_";
		public ParserRuleSet rules;
		public byte lastDefaultID = Token.NULL;
		public byte lastTokenID;
		public byte lastMatchType;
		public int termChar = -1;
		public boolean lastNoLineBreak;
		public boolean lastNoWordBreak;
		public boolean lastIgnoreCase = true;
		public boolean lastHighlightDigits;
		public boolean lastAtLineStart;
		public boolean lastAtWhitespaceEnd;
		public boolean lastAtWordStart;
		public boolean lastRegexp;
		public boolean lastEndRegexp;
		public int lastStartPosMatch;
		public int lastEndPosMatch;
		public String lastDigitRE;
		public String lastHashChar;
		public String lastHashChars;
	}
}

