/***** BEGIN LICENSE BLOCK *****
 * Version: CPL 1.0/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Common Public
 * License Version 1.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.eclipse.org/legal/cpl-v10.html
 *
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 *
 * Copyright (C) 2013 The JRuby Team (jruby@jruby.org)
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either of the GNU General Public License Version 2 or later (the "GPL"),
 * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the CPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the CPL, the GPL or the LGPL.
 ***** END LICENSE BLOCK *****/
package org.jruby.ext.ripper;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import org.jruby.Ruby;
import org.jruby.RubyClass;
import org.jruby.RubyHash;
import org.jruby.RubyNumeric;
import org.jruby.RubyObject;
import org.jruby.anno.JRubyMethod;
import org.jruby.internal.runtime.methods.DynamicMethod;
import org.jruby.lexer.yacc.InputStreamLexerSource;
import org.jruby.lexer.yacc.LexerSource;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.Visibility;
import org.jruby.runtime.builtin.IRubyObject;

public class RubyRipper extends RubyObject {
    public static void initRipper(Ruby runtime) {
        RubyClass ripper = runtime.defineClass("Ripper", runtime.getObject(), new ObjectAllocator() {
            @Override
            public IRubyObject allocate(Ruby runtime, RubyClass klazz) {
                return new RubyRipper(runtime, klazz);
            }
        });
        
        ripper.defineConstant("SCANNER_EVENT_TABLE", createScannerEventTable(runtime, ripper));
        ripper.defineConstant("PARSER_EVENT_TABLE", createParserEventTable(runtime, ripper));
        
        ripper.defineAnnotatedMethods(RubyRipper.class);
    }
    
    // Creates mapping table of token to arity for on_* method calls for the scanner support
    private static IRubyObject createScannerEventTable(Ruby runtime, RubyClass ripper) {
        RubyHash hash = new RubyHash(runtime);

        hash.fastASet(runtime.newSymbol("CHAR"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("__end__"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("backref"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("backtick"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("comma"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("comment"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("const"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("cvar"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("embdoc"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("embdoc_beg"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("embdoc_end"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("embexpr_beg"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("embexpr_end"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("embvar"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("float"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("gvar"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("heredoc_beg"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("heredoc_end"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("ident"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("ignored_nl"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("int"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("ivar"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("kw"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("label"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("lbrace"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("lbracket"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("lparen"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("nl"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("op"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("period"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("qwords_beg"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("rbrace"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("rbracket"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("regexp_beg"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("regexp_end"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("rparen"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("semicolon"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("sp"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("symbeg"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("tlambda"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("tlambeg"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("tstring_beg"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("tstring_content"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("tstring_end"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("words_beg"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("words_sep"), runtime.newFixnum(1));
        
        return hash;
    }
    
    // Creates mapping table of token to arity for on_* method calls for the parser support    
    private static IRubyObject createParserEventTable(Ruby runtime, RubyClass ripper) {
        RubyHash hash = new RubyHash(runtime);

        hash.fastASet(runtime.newSymbol("BEGIN"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("END"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("alias"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("alias_error"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("aref"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("aref_field"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("arg_ambiguous"), runtime.newFixnum(0));
        hash.fastASet(runtime.newSymbol("arg_paren"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("args_add"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("args_add_block"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("args_add_star"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("args_new"), runtime.newFixnum(0));
        hash.fastASet(runtime.newSymbol("array"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("assign"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("assign_error"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("assoc_new"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("assoclist_from_args"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("bare_assoc_hash"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("begin"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("binary"), runtime.newFixnum(3));
        hash.fastASet(runtime.newSymbol("block_var"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("block_var_add_block"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("block_var_add_star"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("blockarg"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("bodystmt"), runtime.newFixnum(4));
        hash.fastASet(runtime.newSymbol("brace_block"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("break"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("call"), runtime.newFixnum(3));
        hash.fastASet(runtime.newSymbol("case"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("class"), runtime.newFixnum(3));
        hash.fastASet(runtime.newSymbol("class_name_error"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("command"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("command_call"), runtime.newFixnum(4));
        hash.fastASet(runtime.newSymbol("const_path_field"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("const_path_ref"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("const_ref"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("def"), runtime.newFixnum(3));
        hash.fastASet(runtime.newSymbol("defined"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("defs"), runtime.newFixnum(5));
        hash.fastASet(runtime.newSymbol("do_block"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("dot2"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("dot3"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("dyna_symbol"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("else"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("elsif"), runtime.newFixnum(3));
        hash.fastASet(runtime.newSymbol("ensure"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("excessed_comma"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("fcall"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("field"), runtime.newFixnum(3));
        hash.fastASet(runtime.newSymbol("for"), runtime.newFixnum(3));
        hash.fastASet(runtime.newSymbol("hash"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("if"), runtime.newFixnum(3));
        hash.fastASet(runtime.newSymbol("if_mod"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("ifop"), runtime.newFixnum(3));
        hash.fastASet(runtime.newSymbol("lambda"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("magic_comment"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("massign"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("method_add_arg"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("method_add_block"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("mlhs_add"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("mlhs_add_star"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("mlhs_new"), runtime.newFixnum(0));
        hash.fastASet(runtime.newSymbol("mlhs_paren"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("module"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("mrhs_add"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("mrhs_add_star"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("mrhs_new"), runtime.newFixnum(0));
        hash.fastASet(runtime.newSymbol("mrhs_new_from_args"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("next"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("opassign"), runtime.newFixnum(3));
        hash.fastASet(runtime.newSymbol("operator_ambiguous"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("param_error"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("params"), runtime.newFixnum(5));
        hash.fastASet(runtime.newSymbol("paren"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("parse_error"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("program"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("qwords_add"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("qwords_new"), runtime.newFixnum(0));
        hash.fastASet(runtime.newSymbol("redo"), runtime.newFixnum(0));
        hash.fastASet(runtime.newSymbol("regexp_add"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("regexp_literal"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("regexp_new"), runtime.newFixnum(0));
        hash.fastASet(runtime.newSymbol("rescue"), runtime.newFixnum(4));
        hash.fastASet(runtime.newSymbol("rescue_mod"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("rest_param"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("retry"), runtime.newFixnum(0));
        hash.fastASet(runtime.newSymbol("return"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("return0"), runtime.newFixnum(0));
        hash.fastASet(runtime.newSymbol("sclass"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("stmts_add"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("stmts_new"), runtime.newFixnum(0));
        hash.fastASet(runtime.newSymbol("string_add"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("string_concat"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("string_content"), runtime.newFixnum(0));
        hash.fastASet(runtime.newSymbol("string_dvar"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("string_embexpr"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("string_literal"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("super"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("symbol"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("symbol_literal"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("top_const_field"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("top_const_ref"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("unary"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("undef"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("unless"), runtime.newFixnum(3));
        hash.fastASet(runtime.newSymbol("unless_mod"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("until"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("until_mod"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("var_alias"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("var_field"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("var_ref"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("vcall"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("void_stmt"), runtime.newFixnum(0));
        hash.fastASet(runtime.newSymbol("when"), runtime.newFixnum(3));
        hash.fastASet(runtime.newSymbol("while"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("while_mod"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("word_add"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("word_new"), runtime.newFixnum(0));
        hash.fastASet(runtime.newSymbol("words_add"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("words_new"), runtime.newFixnum(0));
        hash.fastASet(runtime.newSymbol("xstring_add"), runtime.newFixnum(2));
        hash.fastASet(runtime.newSymbol("xstring_literal"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("xstring_new"), runtime.newFixnum(0));
        hash.fastASet(runtime.newSymbol("yield"), runtime.newFixnum(1));
        hash.fastASet(runtime.newSymbol("yield0"), runtime.newFixnum(0));
        hash.fastASet(runtime.newSymbol("zsuper"), runtime.newFixnum(0));

        return hash;
    }
    
    private RubyRipper(Ruby runtime, RubyClass klazz) {
        super(runtime, klazz);
    }
    
    @JRubyMethod
    public IRubyObject initialize(ThreadContext context, IRubyObject src) {
        return initialize(context, src, null, null);
    }

    @JRubyMethod
    public IRubyObject initialize(ThreadContext context, IRubyObject src, IRubyObject file) {
        return initialize(context, src, file, null);
    }
    
    @JRubyMethod
    public IRubyObject initialize(ThreadContext context, IRubyObject src,IRubyObject file, IRubyObject line) {
        String stringSource = sourceAsString(context, src);
        filename = filenameAsString(context, file);
        int lineno = lineAsInt(context, line);
        ByteArrayInputStream bos = new ByteArrayInputStream(stringSource.getBytes());
        LexerSource source = new InputStreamLexerSource(filename.asJavaString(), bos, null, lineno, true);
        parser = new Ripper19Parser(context, this, source);
         
        return context.runtime.getNil();
    }

    @JRubyMethod
    public IRubyObject column(ThreadContext context) {
        return context.runtime.newFixnum(parser.getColumn());
    }

    @JRubyMethod
    public IRubyObject encoding(ThreadContext context) {
        return null;
    }

    @JRubyMethod(name = "end_seen?")
    public IRubyObject end_seen_p(ThreadContext context) {
        return null;
    }
    
    @JRubyMethod
    public IRubyObject filename(ThreadContext context) {
        return filename;
    }

    @JRubyMethod
    public IRubyObject lineno(ThreadContext context) {
        return context.runtime.newFixnum(parser.getLineno());
    }
    
    @JRubyMethod
    public IRubyObject parse(ThreadContext context) {
        try {
            return (IRubyObject) parser.parse(true);
        } catch (IOException e) {
            System.out.println("ERRROR: " + e);
        }
        return context.runtime.getNil();
    }    

    @JRubyMethod
    public IRubyObject yydebug(ThreadContext context) {
        return null;
    }
    
    @JRubyMethod(name = "yydebug=")
    public IRubyObject yydebug_set(ThreadContext context, IRubyObject arg0) {
        return null;
    }
    
    private String sourceAsString(ThreadContext context, IRubyObject src) {
        // FIXME: respond_to? returns private methods
        DynamicMethod method = src.getMetaClass().searchMethod("gets");
        
        if (method.isUndefined() || method.getVisibility() == Visibility.PRIVATE) {
            return src.convertToString().asJavaString();
        }

        return src.callMethod(context, "gets").asJavaString();
    }
    
    private IRubyObject filenameAsString(ThreadContext context, IRubyObject filename) {
        if (filename == null || filename.isNil()) return context.runtime.newString("(ripper)");
        
        return filename.convertToString();
    }
    
    private int lineAsInt(ThreadContext context, IRubyObject line) {
        if (line == null || line.isNil()) return 0;
        
        return RubyNumeric.fix2int(line.convertToInteger());
    }
    
    private RipperParser parser = null;
    private IRubyObject filename = null;
}
