/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.pdfbox.pdmodel.common.function.type4;

/**
 * Parser for PDF Type 4 functions. This implements a small subset of the PostScript
 * language but is no full PostScript interpreter.
 *
 */
public final class Parser
{

    /** Used to indicate the parsers current state. */
    private enum State
    {
        NEWLINE, WHITESPACE, COMMENT, TOKEN
    }

    private Parser()
    {
        //nop
    }

    /**
     * Parses a Type 4 function and sends the syntactic elements to the given
     * syntax handler.
     * @param input the text source
     * @param handler the syntax handler
     */
    public static void parse(CharSequence input, SyntaxHandler handler)
    {
        Tokenizer tokenizer = new Tokenizer(input, handler);
        tokenizer.tokenize();
    }

    /**
     * This interface defines all possible syntactic elements of a Type 4 function.
     * It is called by the parser as the function is interpreted.
     */
    public interface SyntaxHandler
    {

        /**
         * Indicates that a new line starts.
         * @param text the new line character (CR, LF, CR/LF or FF)
         */
        void newLine(CharSequence text);

        /**
         * Called when whitespace characters are encountered.
         * @param text the whitespace text
         */
        void whitespace(CharSequence text);

        /**
         * Called when a token is encountered. No distinction between operators and values
         * is done here.
         * @param text the token text
         */
        void token(CharSequence text);

        /**
         * Called for a comment.
         * @param text the comment
         */
        void comment(CharSequence text);
    }

    /**
     * Abstract base class for a {@link SyntaxHandler}.
     */
    public abstract static class AbstractSyntaxHandler implements SyntaxHandler
    {

        /** {@inheritDoc} */
        @Override
        public void comment(CharSequence text)
        {
            //nop
        }

        /** {@inheritDoc} */
        @Override
        public void newLine(CharSequence text)
        {
            //nop
        }

        /** {@inheritDoc} */
        @Override
        public void whitespace(CharSequence text)
        {
            //nop
        }

    }

    /**
     * Tokenizer for Type 4 functions.
     */
    private static final class Tokenizer
    {

        private static final char NUL = '\u0000'; //NUL
        private static final char EOT = '\u0004'; //END OF TRANSMISSION
        private static final char TAB = '\u0009'; //TAB CHARACTER
        private static final char FF = '\u000C'; //FORM FEED
        private static final char CR = '\r'; //CARRIAGE RETURN
        private static final char LF = '\n'; //LINE FEED
        private static final char SPACE = '\u0020'; //SPACE

        private final CharSequence input;
        private int index;
        private final SyntaxHandler handler;
        private State state = State.WHITESPACE;
        private final StringBuilder buffer = new StringBuilder();

        private Tokenizer(CharSequence text, SyntaxHandler syntaxHandler)
        {
            this.input = text;
            this.handler = syntaxHandler;
        }

        private boolean hasMore()
        {
            return index < input.length();
        }

        private char currentChar()
        {
            return input.charAt(index);
        }

        private char nextChar()
        {
            index++;
            if (!hasMore())
            {
                return EOT;
            }
            else
            {
                return currentChar();
            }
        }

        private char peek()
        {
            if (index < input.length() - 1)
            {
                return input.charAt(index + 1);
            }
            else
            {
                return EOT;
            }
        }

        private State nextState()
        {
            char ch = currentChar();
            switch (ch)
            {
            case CR:
            case LF:
            case FF: //FF
                state = State.NEWLINE;
                break;
            case NUL:
            case TAB:
            case SPACE:
                state = State.WHITESPACE;
                break;
            case '%':
                state = State.COMMENT;
                break;
            default:
                state = State.TOKEN;
            }
            return state;
        }

        private void tokenize()
        {
            while (hasMore())
            {
                buffer.setLength(0);
                nextState();
                switch (state)
                {
                case NEWLINE:
                    scanNewLine();
                    break;
                case WHITESPACE:
                    scanWhitespace();
                    break;
                case COMMENT:
                    scanComment();
                    break;
                default:
                    scanToken();
                }
            }
        }

        private void scanNewLine()
        {
            assert state == State.NEWLINE;
            char ch = currentChar();
            buffer.append(ch);
            if (ch == CR && peek() == LF)
            {
                //CRLF is treated as one newline
                buffer.append(nextChar());
            }
            handler.newLine(buffer);
            nextChar();
        }

        private void scanWhitespace()
        {
            assert state == State.WHITESPACE;
            buffer.append(currentChar());
            loop:
            while (hasMore())
            {
                char ch = nextChar();
                switch (ch)
                {
                case NUL:
                case TAB:
                case SPACE:
                    buffer.append(ch);
                    break;
                default:
                    break loop;
                }
            }
            handler.whitespace(buffer);
        }

        private void scanComment()
        {
            assert state == State.COMMENT;
            buffer.append(currentChar());
            loop:
            while (hasMore())
            {
                char ch = nextChar();
                switch (ch)
                {
                case CR:
                case LF:
                case FF:
                    break loop;
                default:
                    buffer.append(ch);
                }
            }
            //EOF reached
            handler.comment(buffer);
        }

        private void scanToken()
        {
            assert state == State.TOKEN;
            char ch = currentChar();
            buffer.append(ch);
            switch (ch)
            {
            case '{':
            case '}':
                handler.token(buffer);
                nextChar();
                return;
            default:
                //continue
            }
            loop:
            while (hasMore())
            {
                ch = nextChar();
                switch (ch)
                {
                case NUL:
                case TAB:
                case SPACE:
                case CR:
                case LF:
                case FF:
                case EOT:
                case '{':
                case '}':
                    break loop;
                default:
                    buffer.append(ch);
                }
            }
            //EOF reached
            handler.token(buffer);
        }

    }

}
