package common.tags;

import java.util.ArrayList;
import java.util.Stack;
import java.util.regex.Pattern;

import common.collect.Lists;

class TagInterpreter
{
    private static final Pattern INT_BYTE_ARRAY = Pattern.compile("\\[[-+\\d|,\\s]+\\]");
    private static final Pattern BYTE_ARRAY = Pattern.compile("\\[0x[a-fA-F\\d]*\\]");

    static TagObject parseTag(String tag) throws TagException
    {
        if (!tag.startsWith("{"))
        {
            throw new TagException("Invalid tag encountered, expected \'{\' as first char.");
        }
        else if (countTags(tag) != 1)
        {
            throw new TagException("Encountered multiple top tags, only one expected");
        }
        else
        {
            return (TagObject)parseSection("tag", tag).parse();
        }
    }

    static int countTags(String str) throws TagException
    {
        int count = 0;
        boolean quote = false;
        Stack<Character> stack = new Stack();

        for (int z = 0; z < str.length(); z++)
        {
            char ch = str.charAt(z);

            if (ch == '"')
            {
                if (isEscaped(str, z))
                {
                    if (!quote)
                    {
                        throw new TagException("Illegal use of \\\": " + str);
                    }
                }
                else
                {
                    quote = !quote;
                }
            }
            else if (!quote)
            {
                if (ch != '{' && ch != '[')
                {
                    if (ch == '}' && (stack.isEmpty() || ((Character)stack.pop()).charValue() != '{'))
                    {
                        throw new TagException("Unbalanced curly brackets {}: " + str);
                    }

                    if (ch == ']' && (stack.isEmpty() || ((Character)stack.pop()).charValue() != '['))
                    {
                        throw new TagException("Unbalanced square brackets []: " + str);
                    }
                }
                else
                {
                    if (stack.isEmpty())
                    {
                        ++count;
                    }

                    stack.push(Character.valueOf(ch));
                }
            }
        }

        if (quote)
        {
            throw new TagException("Unbalanced quotation: " + str);
        }
        else if (!stack.isEmpty())
        {
            throw new TagException("Unbalanced brackets: " + str);
        }
        else
        {
            if (count == 0 && !str.isEmpty())
            {
                count = 1;
            }

            return count;
        }
    }

    static TagInterpreter.Parser parseSection(String id, String str) throws TagException
    {
        str = str.trim();

        if (str.startsWith("{"))
        {
            str = str.substring(1, str.length() - 1);
            TagInterpreter.Compound tag;
            String section;

            for (tag = new TagInterpreter.Compound(id); str.length() > 0; str = str.substring(section.length() + 1))
            {
                section = splitSection(str, true);

                if (section.length() > 0)
                {
                    tag.parsers.add(parseSubsection(section, false));
                }

                if (str.length() < section.length() + 1)
                {
                    break;
                }

                char ch = str.charAt(section.length());

                if (ch != ',' && ch != '{' && ch != '}' && ch != '[' && ch != ']')
                {
                    throw new TagException("Unexpected token \'" + ch + "\' at: " + str.substring(section.length()));
                }
            }

            return tag;
        }
        else if (str.startsWith("[") && !INT_BYTE_ARRAY.matcher(str).matches() && !BYTE_ARRAY.matcher(str).matches())
        {
            str = str.substring(1, str.length() - 1);
            TagInterpreter.List list;
            String section;

            for (list = new TagInterpreter.List(id); str.length() > 0; str = str.substring(section.length() + 1))
            {
                section = splitSection(str, false);

                if (section.length() > 0)
                {
                    list.parsers.add(parseSubsection(section, true));
                }

                if (str.length() < section.length() + 1)
                {
                    break;
                }

                char ch = str.charAt(section.length());

                if (ch != ',' && ch != '{' && ch != '}' && ch != '[' && ch != ']')
                {
                    throw new TagException("Unexpected token \'" + ch + "\' at: " + str.substring(section.length()));
                }
            }

            return list;
        }
        else
        {
            return new TagInterpreter.Primitive(id, str);
        }
    }

    private static TagInterpreter.Parser parseSubsection(String str, boolean list) throws TagException
    {
        String key = getKey(str, list);
        String value = getValue(str, list);
        return parseSection(key, value);
    }

    private static String splitSection(String str, boolean noList) throws TagException
    {
        int colon = findSeparator(str, ':');
        int comma = findSeparator(str, ',');

        if (noList)
        {
            if (colon == -1)
            {
                throw new TagException("Unable to locate name/value separator for string: " + str);
            }

            if (comma != -1 && comma < colon)
            {
                throw new TagException("Name error at: " + str);
            }
        }
        else if (colon == -1 || colon > comma)
        {
            colon = -1;
        }

        return split(str, colon);
    }

    private static String split(String str, int index) throws TagException
    {
        Stack<Character> stack = new Stack();
        int idx = index + 1;
        boolean quote = false;
        boolean quoteFirst = false;
        boolean printable = false;

        for (int quoteEnd = 0; idx < str.length(); ++idx)
        {
            char ch = str.charAt(idx);

            if (ch == '"')
            {
                if (isEscaped(str, idx))
                {
                    if (!quote)
                    {
                        throw new TagException("Illegal use of \\\": " + str);
                    }
                }
                else
                {
                    quote = !quote;

                    if (quote && !printable)
                    {
                        quoteFirst = true;
                    }

                    if (!quote)
                    {
                        quoteEnd = idx;
                    }
                }
            }
            else if (!quote)
            {
                if (ch != '{' && ch != '[')
                {
                    if (ch == '}' && (stack.isEmpty() || ((Character)stack.pop()).charValue() != '{'))
                    {
                        throw new TagException("Unbalanced curly brackets {}: " + str);
                    }

                    if (ch == ']' && (stack.isEmpty() || ((Character)stack.pop()).charValue() != '['))
                    {
                        throw new TagException("Unbalanced square brackets []: " + str);
                    }

                    if (ch == ',' && stack.isEmpty())
                    {
                        return str.substring(0, idx);
                    }
                }
                else
                {
                    stack.push(Character.valueOf(ch));
                }
            }

            if (!Character.isWhitespace(ch))
            {
                if (!quote && quoteFirst && quoteEnd != idx)
                {
                    return str.substring(0, quoteEnd + 1);
                }

                printable = true;
            }
        }

        return str.substring(0, idx);
    }

    private static String getKey(String str, boolean list) throws TagException
    {
        if (list)
        {
            str = str.trim();

            if (str.startsWith("{") || str.startsWith("["))
            {
                return "";
            }
        }

        int colon = findSeparator(str, ':');

        if (colon == -1)
        {
            if (list)
            {
                return "";
            }
            else
            {
                throw new TagException("Unable to locate name/value separator for string: " + str);
            }
        }
        else
        {
            return str.substring(0, colon).trim();
        }
    }

    private static String getValue(String str, boolean list) throws TagException
    {
        if (list)
        {
            str = str.trim();

            if (str.startsWith("{") || str.startsWith("["))
            {
                return str;
            }
        }

        int colon = findSeparator(str, ':');

        if (colon == -1)
        {
            if (list)
            {
                return str;
            }
            else
            {
                throw new TagException("Unable to locate name/value separator for string: " + str);
            }
        }
        else
        {
            return str.substring(colon + 1).trim();
        }
    }

    private static int findSeparator(String str, char separator)
    {
        int idx = 0;

        for (boolean noQuote = true; idx < str.length(); idx++)
        {
            char ch = str.charAt(idx);

            if (ch == '"')
            {
                if (!isEscaped(str, idx))
                {
                    noQuote = !noQuote;
                }
            }
            else if (noQuote)
            {
                if (ch == separator)
                {
                    return idx;
                }

                if (ch == '{' || ch == '[')
                {
                    return -1;
                }
            }
        }

        return -1;
    }

    private static boolean isEscaped(String str, int index)
    {
        return index > 0 && str.charAt(index - 1) == '\\' && !isEscaped(str, index - 1);
    }

    abstract static class Parser
    {
        protected String section;

        public abstract Tag parse() throws TagException;
    }

    static class Compound extends TagInterpreter.Parser
    {
        protected java.util.List<TagInterpreter.Parser> parsers = Lists.<TagInterpreter.Parser>newArrayList();

        public Compound(String section)
        {
            this.section = section;
        }

        public Tag parse() throws TagException
        {
            TagObject tag = new TagObject();

            for (TagInterpreter.Parser any : this.parsers)
            {
                tag.set(any.section, any.parse());
            }

            return tag;
        }
    }

    static class List extends TagInterpreter.Parser
    {
        protected java.util.List<TagInterpreter.Parser> parsers = Lists.<TagInterpreter.Parser>newArrayList();

        public List(String section)
        {
            this.section = section;
        }

        public Tag parse() throws TagException
        {
            TagList list = null;
            java.util.List<String> strs = null;

            for (TagInterpreter.Parser any : this.parsers)
            {
            	Tag tag = any.parse();
            	if(tag.getType() == TagType.STRING) {
            		if(list != null)
            			throw new TagException("Cannot use mixed types for list: " + any.section);
            		if(strs == null)
                		strs = new ArrayList<String>(this.parsers.size());
                    strs.add(((TagString)tag).getString());
            	}
            	else if(tag.getType() == TagType.OBJECT) {
            		if(strs != null)
            			throw new TagException("Cannot use mixed types for list: " + any.section);
            		if(list == null)
            			list = new TagList(new ArrayList<TagObject>(this.parsers.size()));
                    list.getList().add(((TagObject)tag));
            	}
            	else {
	        		throw new TagException("Type cannot be put in a list: " + any.section);
            	}
            }

            return strs != null ? new TagStringArray(strs.toArray(new String[strs.size()])) : (list != null ? list : new TagList(Lists.newArrayList()));
        }
    }

    static class Primitive extends TagInterpreter.Parser
    {
        private static final Pattern DOUBLE = Pattern.compile("[-+]?[0-9]*\\.?[0-9]+[d|D]");
        private static final Pattern FLOAT = Pattern.compile("[-+]?[0-9]*\\.?[0-9]+[f|F]");
        private static final Pattern BYTE = Pattern.compile("[-+]?[0-9]+[b|B]");
        private static final Pattern LONG = Pattern.compile("[-+]?[0-9]+[l|L]");
        private static final Pattern SHORT = Pattern.compile("[-+]?[0-9]+[s|S]");
        private static final Pattern INTEGER = Pattern.compile("[-+]?[0-9]+");
        private static final Pattern DOUBLE_UNTYPED = Pattern.compile("[-+]?[0-9]*\\.?[0-9]+");
        
        protected String value;

        public Primitive(String section, String value)
        {
            this.section = section;
            this.value = value;
        }

        public Tag parse() throws TagException
        {
            try
            {
                if (DOUBLE.matcher(this.value).matches())
                {
                    return new TagDouble(Double.parseDouble(this.value.substring(0, this.value.length() - 1)));
                }

                if (FLOAT.matcher(this.value).matches())
                {
                    return new TagFloat(Float.parseFloat(this.value.substring(0, this.value.length() - 1)));
                }

                if (BYTE.matcher(this.value).matches())
                {
                    return new TagByte(Byte.parseByte(this.value.substring(0, this.value.length() - 1)));
                }

                if (LONG.matcher(this.value).matches())
                {
                    return new TagLong(Long.parseLong(this.value.substring(0, this.value.length() - 1)));
                }

                if (SHORT.matcher(this.value).matches())
                {
                    return new TagShort(Short.parseShort(this.value.substring(0, this.value.length() - 1)));
                }

                if (INTEGER.matcher(this.value).matches())
                {
                    return new TagInt(Integer.parseInt(this.value));
                }

                if (DOUBLE_UNTYPED.matcher(this.value).matches())
                {
                    return new TagDouble(Double.parseDouble(this.value));
                }

                if (this.value.equalsIgnoreCase("true") || this.value.equalsIgnoreCase("false"))
                {
                    return new TagBool(Boolean.parseBoolean(this.value));
                }
            }
            catch (NumberFormatException e)
            {
                this.value = this.value.replaceAll("\\\\\"", "\"");
                return new TagString(this.value);
            }

            if (this.value.startsWith("[") && this.value.endsWith("]"))
            {
                String str = this.value.substring(1, this.value.length() - 1);
                try
                {
                	if(str.startsWith("0x")) {
                		str = str.substring(2);
                		if((str.length() & 1) == 1)
                			str = "0" + str;
                		byte[] bytes = new byte[str.length() / 2];
	                    for (int z = 0; z < bytes.length; z++)
	                    {
	                    	bytes[z] = (byte)Integer.parseUnsignedInt(str.substring(z * 2, (z + 1) * 2), 16);
	                    }
	                    return new TagByteArray(bytes);
                	}
                	else {
                        String[] elems = str.split(",");
	                    int[] ints = new int[elems.length];
	                    for (int z = 0; z < elems.length; z++)
	                    {
	                        ints[z] = Integer.parseInt(elems[z].trim());
	                    }
	                    return new TagIntArray(ints);
                	}
                }
                catch (NumberFormatException e)
                {
                    return new TagString(this.value);
                }
            }
            else
            {
                if (this.value.startsWith("'") && this.value.endsWith("'"))
                {
                    String str = this.value.substring(1, this.value.length() - 1);
                    if(str.length() == 1 && str.charAt(0) != '\\') {
                    	return new TagChar(str.charAt(0));
                    }
                    else if(str.length() == 2 && str.charAt(0) == '\\') {
                    	switch(str.charAt(1)) {
	                		case '\\':
	                        	return new TagChar('\\');
	                		case 'n':
	                        	return new TagChar('\n');
                    	}
                    }
                    else if((str.length() == 4 && str.startsWith("\\x")) || (str.length() == 6 && str.startsWith("\\u"))) {
                    	try {
                    		return new TagChar((char)Integer.parseUnsignedInt(str.substring(2), 16));
                    	}
                    	catch(NumberFormatException e) {
                    	}
                    }
                }
                
                if (this.value.startsWith("\"") && this.value.endsWith("\""))
                {
                    this.value = this.value.substring(1, this.value.length() - 1);
                }

                this.value = this.value.replaceAll("\\\\\"", "\"");
                StringBuilder sb = new StringBuilder();

                for (int z = 0; z < this.value.length(); z++)
                {
                    if (z < this.value.length() - 1 && this.value.charAt(z) == '\\' && this.value.charAt(z + 1) == '\\')
                    {
                        sb.append('\\');
                        ++z;
                    }
                    else
                    {
                        sb.append(this.value.charAt(z));
                    }
                }

                return new TagString(sb.toString());
            }
        }
    }
}
