/*
 * 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.jackrabbit.server.remoting.davex;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.InputStream;
import java.io.InputStreamReader;

/** <code>DiffParser</code>... */
class DiffParser {

    // TODO: review again: currently all line-sep. chars before an diff-char are
    // TODO: ignored unless they are escaped in way the handler understands (e.g.
    // TODO: JSON does: \\r for \r).
    // TODO: in contrast line sep. at the end of the string are treated as value.
    // TODO: ... similar: line sep. following by non-diff symbol.

    private final DiffHandler handler;

    private static final int EOF = -1;

    private static final char SYMBOL_ADD_NODE = '+';
    private static final char SYMBOL_MOVE = '>';
    private static final char SYMBOL_REMOVE = '-';
    private static final char SYMBOL_SET_PROPERTY = '^';

    private static final int STATE_START_LINE = 0;
    private static final int STATE_START_TARGET = 1;
    private static final int STATE_TARGET = 2;
    private static final int STATE_START_VALUE = 3;
    private static final int STATE_VALUE = 4;

    /**
     *
     * @param handler
     */
    public DiffParser(DiffHandler handler) {
        this.handler = handler;
    }

    /**
     *
     * @param str
     * @throws IOException
     * @throws DiffException
     */
    public void parse(String str) throws IOException, DiffException {
        parse(new BufferedReader(new StringReader(str)));
    }

    /**
     *
     * @param input
     * @param charSetName
     * @throws IOException
     * @throws DiffException
     */
    public void parse(InputStream input, String charSetName) throws IOException, DiffException {
        parse(new BufferedReader(new InputStreamReader(input, charSetName)));
    }

    /**
     *
     * @param reader
     * @throws IOException
     * @throws DiffException
     */
    public void parse(Reader reader) throws IOException, DiffException {
        int action = -1;
        String path = null;

        StringBuffer lineSeparator = null;
        StringBuffer bf = null;

        int state = STATE_START_LINE;
        int next = reader.read();

        while (next != EOF) {
            switch (state) {
                case STATE_START_LINE:
                    if (isSymbol(next)) {
                        // notify the last action read
                        if (action > -1) {
                            informAction(action, path, bf);
                        }
                        // ... and  start recording the next action
                        action = next;
                        bf = null;
                        lineSeparator = null;
                        state = STATE_START_TARGET;
                    } else if (isLineSeparator(next)) {
                        // still line-separator -> append c to the lineSeparator
                        // buffer and keep state set to STATE_START_LINE
                        if (lineSeparator == null) {
                            throw new DiffException("Invalid start of new line.");
                        } else {
                            lineSeparator.append((char) next);
                        }
                    } else if (lineSeparator != null && bf != null) {
                        // append the collected return/linefeed chars as part
                        // of the value read and continued reading value.
                        bf.append(lineSeparator);
                        bf.append((char) next);
                        lineSeparator = null;
                        state = STATE_VALUE;
                    } else {
                        throw new DiffException("Invalid start of new line.");
                    }
                    break;

                case STATE_START_TARGET:
                    if (Character.isWhitespace((char) next) || next == ':') {
                        throw new DiffException("Invalid start of target path '" + next + "'");
                    }
                    bf = new StringBuffer();
                    bf.append((char) next);
                    state = STATE_TARGET;
                    break;

                case STATE_TARGET:
                    if (Character.isWhitespace((char) next) && endsWithDelim(bf)) {
                        // a sequence of 'wsp:wsp' indicates the delimiter between
                        // the target path and the diff value.
                        path = bf.substring(0, bf.lastIndexOf(":")).trim();
                        state = STATE_START_VALUE;
                        // reset buffer
                        bf = null;
                    } else {
                        // continue reading the path into the buffer.
                        bf.append((char) next);
                    }
                    break;

                case STATE_START_VALUE:
                    if (isLineSeparator(next)) {
                        lineSeparator = new StringBuffer();
                        lineSeparator.append((char) next);
                        bf = new StringBuffer();
                        state = STATE_START_LINE;
                    } else {
                        bf = new StringBuffer();
                        bf.append((char) next);
                        state = STATE_VALUE;
                    }
                    break;

                case STATE_VALUE:
                    if (isLineSeparator(next)) {
                        lineSeparator = new StringBuffer();
                        lineSeparator.append((char) next);
                        state = STATE_START_LINE;
                    } else {
                        bf.append((char) next);
                        // keep state set to STATE_VALUE
                    }
                    break;

            }
            // read the next character.
            next = reader.read();
        }

        // a diff ending after a command or within the target is invalid.
        if (state == STATE_START_TARGET || state == STATE_TARGET) {
            throw new DiffException("Invalid end of DIFF string: missing separator and value.");
        }
        if (state == STATE_START_VALUE ) {
            // line separator AND buffer must be null
            if (!(lineSeparator == null && bf == null)) {
                throw new DiffException("Invalid end of DIFF string.");
            }
        }

        // append eventual remaining line-separators to the value
        if (lineSeparator != null) {
            bf.append(lineSeparator);
        }
        // notify the last action read
        informAction(action, path, bf);
    }

    private void informAction(int action, String path, StringBuffer diffVal) throws DiffException {
        if (path == null) {
            throw new DiffException("Missing path for action " + action + "(diffValue = '"+ diffVal +"')");
        }
        String value = (diffVal == null) ? null : diffVal.toString();
        switch (action) {
            case SYMBOL_ADD_NODE:
                handler.addNode(path, value);
                break;
            case SYMBOL_SET_PROPERTY:
                handler.setProperty(path, value);
                break;
            case SYMBOL_MOVE:
                handler.move(path, value);
                break;
            case SYMBOL_REMOVE:
                handler.remove(path, value);
                break;
            default:
                throw new DiffException("Invalid action " + action);
        }
    }

    private static boolean isSymbol(int c) {
        return c == SYMBOL_ADD_NODE || c == SYMBOL_SET_PROPERTY || c == SYMBOL_MOVE || c == SYMBOL_REMOVE;
    }

    private static boolean isLineSeparator(int c) {
        return c == '\n' || c == '\r';

    }
    private static boolean endsWithDelim(StringBuffer bf) {
        if (bf.length() < 2) {
            return false;
        } else {
            return ':' == bf.charAt(bf.length()-1) && Character.isWhitespace(bf.charAt(bf.length()-2));
        }
    }
}
