/*
 * $Header: /home/cvspublic/jakarta-slide/proposals/tamino/src/util/org/apache/slide/util/jinx/Expression.java,v 1.1 2004/03/25 16:18:11 juergen Exp $
 * $Revision: 1.1 $
 * $Date: 2004/03/25 16:18:11 $
 *
 * ====================================================================
 *
 * Copyright 1999-2004 The Apache Software Foundation.
 *
 * 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.apache.slide.util.jinx;

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import org.apache.slide.util.Strings;
import org.apache.slide.util.cli.Abort;
import org.apache.slide.util.reflect.Function;
import org.apache.slide.util.reflect.Library;
import org.apache.slide.util.reflect.Util;
import org.apache.tools.ant.Project;

/**
 Expression.
<pre>
 expr     = assign | call
 assign   = property "=" call
 call     = dest args
 arg      = atom"?"?
 atom     = property | target | number | string
 property = name
 target   = "!" name
 number   = (0-9)+
 string   = """ ... """
</pre>
 */
public class Expression {
    private final Project project;
    
    public Expression(Project project) {
        this.project = project;
    }
    
    public void eval(String text) throws Abort {
        String str;
        int idx;
        
        str = project.replaceProperties(text);
        idx = str.indexOf('=');
        if (idx == -1) {
            evalCall(str);
        } else {
            evalAssign(str.substring(0, idx).trim(), str.substring(idx + 1).trim());
        }
    }
    
    private void evalAssign(String property, String call) throws Abort {
        Object result;
        
        if (project.getProperty(property) != null) {
            throw new Abort("property already assigned: " + property);
        }
        result = evalCall(call);
        if (result == null) {
            return; // null, don't assign
        }
        Scope.global.add(property, result);
        project.setProperty(property, convertToProperty(result));
    }
    
    private Object evalCall(String call) throws Abort {
        int idx;
        Object[] tmp;
        Function fn;
        Object[] args;
        Throwable t;
        
        call = call.trim();
        idx = call.indexOf(" ");
        if (idx == -1) {
            idx = call.length();
            args = new Object[] {};
        } else {
            args = evalArguments(call.substring(idx + 1));
        }
        tmp = evalDest(call.substring(0, idx), args);
        fn = (Function) tmp[0];
        args = (Object[]) tmp[1];
        try {
            return fn.invoke(args);
        } catch (InvocationTargetException e) {
            t = e.getTargetException();
            if (t instanceof RuntimeException) {
                t.printStackTrace();
            }
            throw new Abort(fn.getName() + " failed: " + t.getMessage());
        }
    }
    
    private static String convertToProperty(Object value) {
        if (value instanceof String) {
            return (String) value;
        } else if (value instanceof File) {
            return ((File) value).getAbsolutePath();
        } else if (value != null) {
            return value.toString();
        } else {
            return null;
        }
    }
    
    private Object[] evalDest(String dest, Object[] actuals) throws Abort {
        int idx;
        Object obj;
        Function[] fns;
        Function fn;
        Resolver resolver;
        String objName;
        String methName;
        
        try {
            fns = Library.global.getStatic(dest);
            if (fns.length != 0) {
                // don't change actuals
            } else {
                idx = dest.indexOf('.');
                if (idx == -1) {
                    throw new Abort("constructor or method call expected");
                }
                objName = dest.substring(0, idx);
                obj = Scope.global.get(objName);
                if (obj == null) {
                    throw new Abort("unkown object: " + objName);
                }
                methName = dest.substring(idx + 1);
                fns = Library.global.getMethods(obj.getClass(), methName);
                if (fns.length == 0) {
                    throw new Abort("method '" + methName + "' not found in class " + obj.getClass());
                }
                actuals = Util.cons(obj, actuals);
            }
            resolver = new Resolver(project);
            fn = resolver.run(fns, actuals);
        } catch (Abort e) {
            throw new Abort(dest, e);
        }
        return new Object[] { fn, actuals };
    }
    
    private Object[] evalArguments(String argumentsStr) throws Abort {
        String[] arguments;
        Object[] result;
        int i;
        
        arguments = Strings.split(argumentsStr, " ");
        result = new Object[arguments.length];
        for (i = 0; i < arguments.length; i++) {
            result[i] = evalArgument(arguments[i]);
        }
        return result;
    }
    
    private static final char TARGET = '!';
    private static final char PATH = '^';
    private static final char STR = '"';
    private static final char OPT = '?';
    
    private Object evalArgument(String argument) throws Abort {
        boolean opt;
        Object atom;
        
        opt = argument.endsWith("" + OPT);
        if (opt) {
            argument = argument.substring(0, argument.length() - 1);
        }
        atom = evalAtom(argument);
        if (atom == null && !opt) {
            throw new Abort("empty argument: " + argument);
        }
        return atom;
    }
    
    private Object evalAtom(String atom) throws Abort {
        Object obj;
        
        if (atom.startsWith("" + STR)) {
            if (!atom.endsWith("" + STR)) {
                throw new Abort("invalid string constant: " + atom);
            }
            return atom.substring(1, atom.length() - 1);
            // TODO: .replace('_', ' ');
        } else if (atom.startsWith("" + TARGET)) {
            atom = atom.substring(1);
            return project.getTargets().get(atom);
        } else if (atom.startsWith("" + PATH)) {
            atom = atom.substring(1);
            obj = project.getReference(atom);
            if (obj != null) {
                obj = obj.toString();
            }
            return obj;
        } else {
            try {
                Integer.parseInt(atom);
                return atom;
            } catch (NumberFormatException e) {
                // fall through
            }
            obj = Scope.global.get(atom);
            if (obj == null) {
                obj = project.getProperty(atom);
            }
            return obj;
        }
    }
    
    private static class Token {
        public final int type;
        public final String str;
        
        public Token(int type, String str) {
            this.type = type;
            this.str = str;
        }
    }
    private static class Stream {
        private final List lst;
        private int pos;
        
        public Stream() {
            this.lst = new ArrayList();
            this.pos = 0;
        }
        public void add(int type, String str) {
            lst.add(new Token(type, str));
        }
        public int probe() {
            if (eof()) {
                return peek().type;
            } else {
                return -1;
            }
        }
        public Token peek() {
            return (Token) lst.get(pos);
        }
        public void eat() {
            pos++;
        }
        public boolean eof() {
            return pos >= lst.size();
        }
    }
}

