/*
 * Copyright (C) 2011-2015 René Jeschke <rene_jeschke@yahoo.de>
 *
 * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
 *
 * 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.javachina.ext.markdown;

/**
 * Utilities.
 *
 * @author René Jeschke <rene_jeschke@yahoo.de>
 */
class Utils
{
    /** Random number generator value. */
    private static int RND = (int)System.nanoTime();

    /**
     * LCG random number generator.
     *
     * @return A pseudo random number between 0 and 1023
     */
    public final static int rnd()
    {
        return (RND = RND * 1664525 + 1013904223) >>> 22;
    }

    /**
     * Skips spaces in the given String.
     *
     * @param in
     *            Input String.
     * @param start
     *            Starting position.
     * @return The new position or -1 if EOL has been reached.
     */
    public final static int skipSpaces(final String in, final int start)
    {
        int pos = start;
        while (pos < in.length() && (in.charAt(pos) == ' ' || in.charAt(pos) == '\n'))
        {
            pos++;
        }
        return pos < in.length() ? pos : -1;
    }

    /**
     * Processed the given escape sequence.
     *
     * @param out
     *            The StringBuilder to write to.
     * @param ch
     *            The character.
     * @param pos
     *            Current parsing position.
     * @return The new position.
     */
    public final static int escape(final StringBuilder out, final char ch, final int pos)
    {
        switch (ch)
        {
        case '\\':
        case '[':
        case ']':
        case '(':
        case ')':
        case '{':
        case '}':
        case '#':
        case '"':
        case '\'':
        case '.':
        case '>':
        case '<':
        case '*':
        case '+':
        case '-':
        case '_':
        case '!':
        case '`':
        case '^':
            out.append(ch);
            return pos + 1;
        default:
            out.append('\\');
            return pos;
        }
    }

    /**
     * Reads characters until any 'end' character is encountered.
     *
     * @param out
     *            The StringBuilder to write to.
     * @param in
     *            The Input String.
     * @param start
     *            Starting position.
     * @param end
     *            End characters.
     * @return The new position or -1 if no 'end' char was found.
     */
    public final static int readUntil(final StringBuilder out, final String in, final int start, final char... end)
    {
        int pos = start;
        while (pos < in.length())
        {
            final char ch = in.charAt(pos);
            if (ch == '\\' && pos + 1 < in.length())
            {
                pos = escape(out, in.charAt(pos + 1), pos);
            }
            else
            {
                boolean endReached = false;
                for (int n = 0; n < end.length; n++)
                {
                    if (ch == end[n])
                    {
                        endReached = true;
                        break;
                    }
                }
                if (endReached)
                {
                    break;
                }
                out.append(ch);
            }
            pos++;
        }

        return (pos == in.length()) ? -1 : pos;
    }

    /**
     * Reads characters until the 'end' character is encountered.
     *
     * @param out
     *            The StringBuilder to write to.
     * @param in
     *            The Input String.
     * @param start
     *            Starting position.
     * @param end
     *            End characters.
     * @return The new position or -1 if no 'end' char was found.
     */
    public final static int readUntil(final StringBuilder out, final String in, final int start, final char end)
    {
        int pos = start;
        while (pos < in.length())
        {
            final char ch = in.charAt(pos);
            if (ch == '\\' && pos + 1 < in.length())
            {
                pos = escape(out, in.charAt(pos + 1), pos);
            }
            else
            {
                if (ch == end)
                {
                    break;
                }
                out.append(ch);
            }
            pos++;
        }

        return (pos == in.length()) ? -1 : pos;
    }

    /**
     * Reads a markdown link.
     *
     * @param out
     *            The StringBuilder to write to.
     * @param in
     *            Input String.
     * @param start
     *            Starting position.
     * @return The new position or -1 if this is no valid markdown link.
     */
    public final static int readMdLink(final StringBuilder out, final String in, final int start)
    {
        int pos = start;
        int counter = 1;
        while (pos < in.length())
        {
            final char ch = in.charAt(pos);
            if (ch == '\\' && pos + 1 < in.length())
            {
                pos = escape(out, in.charAt(pos + 1), pos);
            }
            else
            {
                boolean endReached = false;
                switch (ch)
                {
                case '(':
                    counter++;
                    break;
                case ' ':
                    if (counter == 1)
                    {
                        endReached = true;
                    }
                    break;
                case ')':
                    counter--;
                    if (counter == 0)
                    {
                        endReached = true;
                    }
                    break;
                }
                if (endReached)
                {
                    break;
                }
                out.append(ch);
            }
            pos++;
        }

        return (pos == in.length()) ? -1 : pos;
    }

    /**
     * Reads a markdown link ID.
     *
     * @param out
     *            The StringBuilder to write to.
     * @param in
     *            Input String.
     * @param start
     *            Starting position.
     * @return The new position or -1 if this is no valid markdown link ID.
     */
    public final static int readMdLinkId(final StringBuilder out, final String in, final int start)
    {
        int pos = start;
        int counter = 1;
        while (pos < in.length())
        {
            final char ch = in.charAt(pos);
            boolean endReached = false;
            switch (ch)
            {
            case '\n':
                out.append(' ');
                break;
            case '[':
                counter++;
                out.append(ch);
                break;
            case ']':
                counter--;
                if (counter == 0)
                {
                    endReached = true;
                }
                else
                {
                    out.append(ch);
                }
                break;
            default:
                out.append(ch);
                break;
            }
            if (endReached)
            {
                break;
            }
            pos++;
        }

        return (pos == in.length()) ? -1 : pos;
    }

    /**
     * Reads characters until any 'end' character is encountered, ignoring
     * escape sequences.
     *
     * @param out
     *            The StringBuilder to write to.
     * @param in
     *            The Input String.
     * @param start
     *            Starting position.
     * @param end
     *            End characters.
     * @return The new position or -1 if no 'end' char was found.
     */
    public final static int readRawUntil(final StringBuilder out, final String in, final int start, final char... end)
    {
        int pos = start;
        while (pos < in.length())
        {
            final char ch = in.charAt(pos);
            boolean endReached = false;
            for (int n = 0; n < end.length; n++)
            {
                if (ch == end[n])
                {
                    endReached = true;
                    break;
                }
            }
            if (endReached)
            {
                break;
            }
            out.append(ch);
            pos++;
        }

        return (pos == in.length()) ? -1 : pos;
    }

    /**
     * Reads characters until the end character is encountered, taking care of
     * HTML/XML strings.
     *
     * @param out
     *            The StringBuilder to write to.
     * @param in
     *            The Input String.
     * @param start
     *            Starting position.
     * @param end
     *            End characters.
     * @return The new position or -1 if no 'end' char was found.
     */
    public final static int readRawUntil(final StringBuilder out, final String in, final int start, final char end)
    {
        int pos = start;
        while (pos < in.length())
        {
            final char ch = in.charAt(pos);
            if (ch == end)
            {
                break;
            }
            out.append(ch);
            pos++;
        }

        return (pos == in.length()) ? -1 : pos;
    }

    /**
     * Reads characters until any 'end' character is encountered, ignoring
     * escape sequences.
     *
     * @param out
     *            The StringBuilder to write to.
     * @param in
     *            The Input String.
     * @param start
     *            Starting position.
     * @param end
     *            End characters.
     * @return The new position or -1 if no 'end' char was found.
     */
    public final static int readXMLUntil(final StringBuilder out, final String in, final int start, final char... end)
    {
        int pos = start;
        boolean inString = false;
        char stringChar = 0;
        while (pos < in.length())
        {
            final char ch = in.charAt(pos);
            if (inString)
            {
                if (ch == '\\')
                {
                    out.append(ch);
                    pos++;
                    if (pos < in.length())
                    {
                        out.append(ch);
                        pos++;
                    }
                    continue;
                }
                if (ch == stringChar)
                {
                    inString = false;
                    out.append(ch);
                    pos++;
                    continue;
                }
            }
            switch (ch)
            {
            case '"':
            case '\'':
                inString = true;
                stringChar = ch;
                break;
            }
            if (!inString)
            {
                boolean endReached = false;
                for (int n = 0; n < end.length; n++)
                {
                    if (ch == end[n])
                    {
                        endReached = true;
                        break;
                    }
                }
                if (endReached)
                {
                    break;
                }
            }
            out.append(ch);
            pos++;
        }

        return (pos == in.length()) ? -1 : pos;
    }

    /**
     * Appends the given string encoding special HTML characters.
     *
     * @param out
     *            The StringBuilder to write to.
     * @param in
     *            Input String.
     * @param start
     *            Input String starting position.
     * @param end
     *            Input String end position.
     */
    public final static void appendCode(final StringBuilder out, final String in, final int start, final int end)
    {
        for (int i = start; i < end; i++)
        {
            final char c;
            switch (c = in.charAt(i))
            {
            case '&':
                out.append("&amp;");
                break;
            case '<':
                out.append("&lt;");
                break;
            case '>':
                out.append("&gt;");
                break;
            default:
                out.append(c);
                break;
            }
        }
    }

    /**
     * Appends the given string encoding special HTML characters (used in HTML
     * attribute values).
     *
     * @param out
     *            The StringBuilder to write to.
     * @param in
     *            Input String.
     * @param start
     *            Input String starting position.
     * @param end
     *            Input String end position.
     */
    public final static void appendValue(final StringBuilder out, final String in, final int start, final int end)
    {
        for (int i = start; i < end; i++)
        {
            final char c;
            switch (c = in.charAt(i))
            {
            case '&':
                out.append("&amp;");
                break;
            case '<':
                out.append("&lt;");
                break;
            case '>':
                out.append("&gt;");
                break;
            case '"':
                out.append("&quot;");
                break;
            case '\'':
                out.append("&apos;");
                break;
            default:
                out.append(c);
                break;
            }
        }
    }

    /**
     * Append the given char as a decimal HTML entity.
     *
     * @param out
     *            The StringBuilder to write to.
     * @param value
     *            The character.
     */
    public final static void appendDecEntity(final StringBuilder out, final char value)
    {
        out.append("&#");
        out.append((int)value);
        out.append(';');
    }

    /**
     * Append the given char as a hexadecimal HTML entity.
     *
     * @param out
     *            The StringBuilder to write to.
     * @param value
     *            The character.
     */
    public final static void appendHexEntity(final StringBuilder out, final char value)
    {
        out.append("&#x");
        out.append(Integer.toHexString(value));
        out.append(';');
    }

    /**
     * Appends the given mailto link using obfuscation.
     *
     * @param out
     *            The StringBuilder to write to.
     * @param in
     *            Input String.
     * @param start
     *            Input String starting position.
     * @param end
     *            Input String end position.
     */
    public final static void appendMailto(final StringBuilder out, final String in, final int start, final int end)
    {
        for (int i = start; i < end; i++)
        {
            final char c;
            final int r = rnd();
            switch (c = in.charAt(i))
            {
            case '&':
            case '<':
            case '>':
            case '"':
            case '\'':
            case '@':
                if (r < 512)
                {
                    appendDecEntity(out, c);
                }
                else
                {
                    appendHexEntity(out, c);
                }
                break;
            default:
                if (r < 32)
                {
                    out.append(c);
                }
                else if (r < 520)
                {
                    appendDecEntity(out, c);
                }
                else
                {
                    appendHexEntity(out, c);
                }
                break;
            }
        }
    }

    /**
     * Extracts the tag from an XML element.
     *
     * @param out
     *            The StringBuilder to write to.
     * @param in
     *            Input StringBuilder.
     */
    public final static void getXMLTag(final StringBuilder out, final StringBuilder in)
    {
        int pos = 1;
        if (in.charAt(1) == '/')
        {
            pos++;
        }
        while (Character.isLetterOrDigit(in.charAt(pos)))
        {
            out.append(in.charAt(pos++));
        }
    }

    /**
     * Extracts the tag from an XML element.
     *
     * @param out
     *            The StringBuilder to write to.
     * @param in
     *            Input String.
     */
    public final static void getXMLTag(final StringBuilder out, final String in)
    {
        int pos = 1;
        if (in.charAt(1) == '/')
        {
            pos++;
        }
        while (Character.isLetterOrDigit(in.charAt(pos)))
        {
            out.append(in.charAt(pos++));
        }
    }

    /**
     * Reads an XML element.
     *
     * @param out
     *            The StringBuilder to write to.
     * @param in
     *            Input String.
     * @param start
     *            Starting position.
     * @param safeMode
     *            Whether to escape unsafe HTML tags or not
     * @return The new position or -1 if this is no valid XML element.
     */
    public final static int readXML(final StringBuilder out, final String in, final int start, final boolean safeMode)
    {
        int pos;
        final boolean isCloseTag;
        try
        {
            if (in.charAt(start + 1) == '/')
            {
                isCloseTag = true;
                pos = start + 2;
            }
            else if (in.charAt(start + 1) == '!')
            {
                out.append("<!");
                return start + 1;
            }
            else
            {
                isCloseTag = false;
                pos = start + 1;
            }
            if (safeMode)
            {
                final StringBuilder temp = new StringBuilder();
                pos = readXMLUntil(temp, in, pos, ' ', '/', '>');
                if (pos == -1)
                {
                    return -1;
                }
                final String tag = temp.toString().trim().toLowerCase();
                if (HTML.isUnsafeHtmlElement(tag))
                {
                    out.append("&lt;");
                }
                else
                {
                    out.append("<");
                }
                if (isCloseTag)
                {
                    out.append('/');
                }
                out.append(temp);
            }
            else
            {
                out.append('<');
                if (isCloseTag)
                {
                    out.append('/');
                }
                pos = readXMLUntil(out, in, pos, ' ', '/', '>');
            }
            if (pos == -1)
            {
                return -1;
            }
            pos = readXMLUntil(out, in, pos, '/', '>');
            if (in.charAt(pos) == '/')
            {
                out.append(" /");
                pos = readXMLUntil(out, in, pos + 1, '>');
                if (pos == -1)
                {
                    return -1;
                }
            }
            if (in.charAt(pos) == '>')
            {
                out.append('>');
                return pos;
            }
        }
        catch (final StringIndexOutOfBoundsException e)
        {
            return -1;
        }
        return -1;
    }

    /**
     * Appends the given string to the given StringBuilder, replacing '&amp;',
     * '&lt;' and '&gt;' by their respective HTML entities.
     *
     * @param out
     *            The StringBuilder to append to.
     * @param value
     *            The string to append.
     * @param offset
     *            The character offset into value from where to start
     */
    public final static void codeEncode(final StringBuilder out, final String value, final int offset)
    {
        for (int i = offset; i < value.length(); i++)
        {
            final char c = value.charAt(i);
            switch (c)
            {
            case '&':
                out.append("&amp;");
                break;
            case '<':
                out.append("&lt;");
                break;
            case '>':
                out.append("&gt;");
                break;
            default:
                out.append(c);
            }
        }
    }

    /**
     * Removes trailing <code>`</code> or <code>~</code> and trims spaces.
     *
     * @param fenceLine
     *            Fenced code block starting line
     * @return Rest of the line after trimming and backtick or tilde removal
     * @since 0.7
     */
    public final static String getMetaFromFence(final String fenceLine)
    {
        for (int i = 0; i < fenceLine.length(); i++)
        {
            final char c = fenceLine.charAt(i);
            if (!Character.isWhitespace(c) && c != '`' && c != '~')
            {
                return fenceLine.substring(i).trim();
            }
        }
        return "";
    }
}
