/*
 * Copyright 2004-2012 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.mayaa.impl.cycle.script.rhino.direct;

import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.NativeObject;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.Undefined;
import org.seasar.mayaa.PositionAware;
import org.seasar.mayaa.cycle.ServiceCycle;
import org.seasar.mayaa.cycle.scope.AttributeScope;
import org.seasar.mayaa.engine.specification.PrefixAwareName;
import org.seasar.mayaa.engine.specification.QName;
import org.seasar.mayaa.impl.CONST_IMPL;
import org.seasar.mayaa.impl.cycle.CycleUtil;
import org.seasar.mayaa.impl.cycle.script.AbstractTextCompiledScript;
import org.seasar.mayaa.impl.cycle.script.ReadOnlyScriptBlockException;
import org.seasar.mayaa.impl.cycle.script.rhino.OffsetLineRhinoException;
import org.seasar.mayaa.impl.cycle.script.rhino.RhinoUtil;
import org.seasar.mayaa.impl.engine.specification.PrefixAwareNameImpl;
import org.seasar.mayaa.impl.util.ObjectUtil;
import org.seasar.mayaa.impl.util.StringUtil;

/**
 * スコープから変数を取得するだけの処理をするスクリプトの抽象クラス。
 *
 * @author Koji Suga (Gluegent Inc.)
 */
public abstract class AbstractGetterScript extends AbstractTextCompiledScript {

    private static final long serialVersionUID = 1L;

    private static final Log LOG = LogFactory.getLog(AbstractGetterScript.class);

    private static final String[] SERVICE_CYCLE_PROPERTY_NAMES =
        ObjectUtil.getPropertyNames(ServiceCycle.class);

    protected final String _sourceName;
    protected final int _lineNumber;
    protected final int _offsetLine;

    protected final String _scopeName;
    protected final String _attributeName;
    protected final String _propertyName;
    protected final String _beanPropertyName;

    public AbstractGetterScript(
            String text, PositionAware position, int offsetLine, String scopeName,
            String attributeName, String propertyName, String[] beanPropertyNames) {
        super(text);
        String sourceName = position.getSystemID();
        if (position instanceof PrefixAwareName) {
            PrefixAwareName prefixAwareName = (PrefixAwareName) position;
            QName qName = prefixAwareName.getQName();
            if (CONST_IMPL.URI_MAYAA == qName.getNamespaceURI()) {
                sourceName += "#" + qName.getLocalName();
            } else {
                sourceName += "#"
                    + PrefixAwareNameImpl.forPrefixAwareNameString(qName,
                        prefixAwareName.getPrefix());
            }
        }
        _sourceName = sourceName;
        _lineNumber = position.getLineNumber();
        _offsetLine = offsetLine;

        _scopeName = scopeName;
        _attributeName = attributeName;
        _propertyName = propertyName;
        _beanPropertyName = (contains(beanPropertyNames, attributeName)) ?
                attributeName : null;
    }

    private boolean contains(String[] array, String test) {
        if (array != null) {
            for (int i = 0; i < array.length; i++) {
                if (array[i].equals(test)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * スクリプトを実行した結果を取得します。
     * このメソッドではスクリプトをエミュレートし、スコープからの属性取得および
     * スコープのプロパティを取得して返します。
     *
     * @param args 引数。使用しない。
     * @return 変数の値を返します。見つからない場合はnullを返します。
     * @throws OffsetLineRhinoException 属性がnullで、プロパティを取得しようとしたとき。
     */
    public Object execute(Object[] args) {
        Context cx = RhinoUtil.enter();
        try {
            Object result = getAttribute();

            if (_propertyName != null) {
                if (result == null) {
                    String script = getText();
                    String message = StringUtil.getMessage(
                            AbstractGetterScript.class, 1, _propertyName, script);
                    int[] position = extractLineSourcePosition(script, _propertyName);
                    String lineSource = script.substring(position[0], position[1]);
                    throw new OffsetLineRhinoException(
                            message, _sourceName, _lineNumber, lineSource,
                            lineSource.indexOf(_propertyName), _offsetLine, null);
                }

                result = readProperty(result);
            }

            return RhinoUtil.convertResult(cx, getExpectedClass(), result);
        } finally {
            Context.exit();
        }
    }

    /**
     * attributeからpropertyを読み出して返します。
     *
     * @param attribute propertyを読み出す属性
     * @return propertyの値、またはnull
     */
    private Object readProperty(Object attribute) {
        Object property = Undefined.instance;
        if (attribute instanceof NativeObject) {
            // Rhinoで作成したオブジェクトの場合
            NativeObject nativeObject = (NativeObject) attribute;
            Object nativeProperty = nativeObject.get(_propertyName, nativeObject);
            if (nativeProperty != Scriptable.NOT_FOUND) {
                property = nativeProperty;
            }
        } else if (attribute instanceof AttributeScope) {
            // TODO __current__ とか __parent__ とかの場合
            // スコープの場合
            AttributeScope scope = (AttributeScope) attribute;
            property = scope.getAttribute(_propertyName);
        } else if (attribute instanceof Map) {
            property = ((Map) attribute).get(_propertyName);
        } else {
            // それ以外はBeanとしてのアクセス
            try {
                property = RhinoUtil.getSimpleProperty(attribute, _propertyName);
            } catch (RuntimeException ignore) {
                // undefined
                LOG.debug(StringUtil.getMessage(AbstractGetterScript.class, 2,
                        _propertyName, attribute.getClass().getName()));
            }
        }
        return property;
    }

    protected static int[] extractLineSourcePosition(String text, String propertyName) {
        if (text.indexOf('\n') == -1) {
            return new int[] { 0, text.length() };
        }
        int index = text.lastIndexOf(propertyName);
        int head = text.lastIndexOf('\n', index);
        int tail = text.indexOf('\n', index + propertyName.length());
        if (tail == -1) {
            tail = text.length();
        }
        return new int[] { head + 1, tail };
    }

    public String getAttributeName(int index) {
        return _attributeName;
    }

    public String getAttributeName() {
        return _attributeName;
    }

    public String getPropertyName() {
        return _propertyName;
    }

    /**
     * {@link #execute(Object[])}で呼び出され、スコープから属性値を取得します。
     * 指定された属性名がスコープのプロパティを指している場合はプロパティの値を
     * 返します。
     *
     * @return 属性値
     */
    protected Object getAttribute() {
        AttributeScope scope = getScope();
        if (scope == null) {
            if (_scopeName == null) {
                for (int i = 0; i < SERVICE_CYCLE_PROPERTY_NAMES.length; i++) {
                    if (SERVICE_CYCLE_PROPERTY_NAMES[i].equals(_attributeName)) {
                        ServiceCycle cycle = CycleUtil.getServiceCycle();
                        return ObjectUtil.getProperty(cycle, _attributeName);
                    }
                }
            }
            return null;
        }

        Object result = scope.getAttribute(_attributeName);
        if (result == null && _beanPropertyName != null) {
            result = ObjectUtil.getProperty(scope, _beanPropertyName);
        }
        return result;
    }

    /**
     * {@link #execute(Object[])}で呼び出されます。
     * GetterScriptの実装はこのメソッドでスコープを返してください。
     *
     * @return スコープ
     */
    protected abstract AttributeScope getScope();

    /**
     * 読み込み専用。
     *
     * @return true
     */
    public boolean isReadOnly() {
        return true;
    }

    /**
     * サポートしない。
     *
     * @param value 値
     * @throws ReadOnlyScriptBlockException
     */
    public void assignValue(Object value) {
        throw new ReadOnlyScriptBlockException(getScriptText());
    }

}
