/*
 * Copyright 2004-2015 the Seasar Foundation and the Others.
 *
 * 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 org.seasar.extension.dxo.util;

/**
 * 変換ルールの{@link SimpleExpression 簡略式}を表すソース文字列を解析します。
 * 
 * @author koichik
 */
public class SimpleExpressionParser {

    /** ソース文字列 */
    protected String source;

    /** ソース文字列の長さ */
    protected int length;

    /** ソース文字列中の未解析の位置 */
    protected int index;

    /** 解析結果 */
    protected SimpleExpression expression;

    /**
     * ソース文字列を解析して、結果の{@link SimpleExpression}を返します。
     * <p>
     * 解析できなかった場合は<code>null</code>を返します。
     * </p>
     * 
     * @param source
     *            ソース文字列
     * @return 解析結果の{@link SimpleExpression}
     */
    public static SimpleExpression parse(final String source) {
        final SimpleExpressionParser parser = new SimpleExpressionParser(source);
        return parser.conversionRuleList();
    }

    /**
     * インスタンスを構築します。
     * 
     * @param source
     *            ソース文字列
     */
    protected SimpleExpressionParser(final String source) {
        super();
        this.source = source;
        this.length = source.length();
        this.expression = new SimpleExpression();
    }

    /**
     * 変換ルールの並びを解析します。
     * 
     * <pre>
     * ConversionRuleList:
     *     ConversionRule
     *     ConversionRuleList , ConversionRule
     * </pre>
     * 
     * @return 解析結果の{@link SimpleExpression}
     */
    protected SimpleExpression conversionRuleList() {
        try {
            conversionRule();
            while (comma()) {
                conversionRule();
            }
            eof();
            return expression;
        } catch (final SimpleExpressionParseException e) {
            return null;
        }
    }

    /**
     * 変換ルールを解析します。
     * 
     * <pre>
     * ConversionRule:
     *     DestProperty : null
     *     DestProperty : SourcePropertyList
     * </pre>
     */
    protected void conversionRule() {
        destProperty();
        colon();
        if (!nullLiteral()) {
            sourcePropertyList();
        }
    }

    /**
     * 次のトークンがカンマなら<code>true</code>を返します。
     * 
     * @return 次のトークンがカンマなら<code>true</code>
     */
    protected boolean comma() {
        return nextChar(',');
    }

    /**
     * 未解析のソース文字列が空白文字だけであることを確認します。
     */
    protected void eof() {
        skipWhiteSpace();
        if (index < length) {
            throw new SimpleExpressionParseException();
        }
    }

    /**
     * 変換先のプロパティ名を解析します。
     * 
     * <pre>
     * DestProperty:
     *     Identifier
     * </pre>
     */
    protected void destProperty() {
        final String name = nextIdentifier();
        expression.addDestProperty(name);
    }

    /**
     * 次のトークンがコロンであることを確認します。
     */
    protected void colon() {
        if (!nextChar(':')) {
            throw new SimpleExpressionParseException();
        }
    }

    /**
     * 変換元プロパティ名の並びを解析します。
     * 
     * <pre>
     * SourcePropertyList:
     *     SourceProperty
     *     SourcePropertyList . SourceProperty
     * </pre>
     */
    protected void sourcePropertyList() {
        sourceProperty();
        while (period()) {
            sourceProperty();
        }
    }

    /**
     * 変換元プロパティ名を解析します。
     * 
     * <pre>
     * SourceProperty:
     *     Identifier
     * </pre>
     */
    protected void sourceProperty() {
        final String value = nextIdentifier();
        expression.addSourceProperty(value);
    }

    /**
     * 次のトークンがリテラル<code>null</code>なら<code>true</code>を返します。
     * 
     * @return 次のトークンがリテラル<code>null</code>なら<code>true</code>
     */
    protected boolean nullLiteral() {
        final int savedIndex = index;
        final String value = nextIdentifier();
        if ("null".equals(value)) {
            expression.addSourceProperty(null);
            return true;
        }
        index = savedIndex;
        return false;
    }

    /**
     * 次のトークンがピリオドなら<code>true</code>を返します。
     * 
     * @return 次のトークンがピリオドなら<code>true</code>
     */
    protected boolean period() {
        return nextChar('.');
    }

    /**
     * 未解析文字列の空白記号を読み飛ばします。
     */
    protected void skipWhiteSpace() {
        for (; index < length; ++index) {
            if (!Character.isWhitespace(source.charAt(index))) {
                return;
            }
        }
    }

    /**
     * 未解析文字列の次の文字が期待値であれば<code>true</code>を返します。
     * 
     * @param expected
     *            期待している文字
     * @return 未解析文字列の次の文字が期待値であれば<code>true</code>
     */
    protected boolean nextChar(final char expected) {
        skipWhiteSpace();
        if (index >= length) {
            return false;
        }
        if (source.charAt(index) == expected) {
            ++index;
            return true;
        }
        return false;
    }

    /**
     * 未解析文字列から識別子を返します。
     * 
     * @return 識別子
     */
    protected String nextIdentifier() {
        skipWhiteSpace();
        if (index >= length
                || !Character.isJavaIdentifierStart(source.charAt(index))) {
            throw new SimpleExpressionParseException();
        }

        final int beginIndex = index;
        for (; index < length; ++index) {
            if (!Character.isJavaIdentifierPart(source.charAt(index))) {
                break;
            }
        }
        final int endIndex = index;
        return source.substring(beginIndex, endIndex);
    }

}
