/***** BEGIN LICENSE BLOCK *****
 * Copyright (c) 2006-2007 Nick Sieger <nick@nicksieger.com>
 * Copyright (c) 2006-2007 Ola Bini <ola.bini@gmail.com>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 * 
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 ***** END LICENSE BLOCK *****/

package jdbc_adapter;

import org.jruby.Ruby;
import org.jruby.RubyModule;
import org.jruby.RubyString;
import org.jruby.RubyFloat;
import org.jruby.RubyFixnum;
import org.jruby.RubyBignum;
import org.jruby.RubyBoolean;
import org.jruby.RubyBigDecimal;
import org.jruby.RubyRange;
import org.jruby.RubyNumeric;

import org.jruby.runtime.builtin.IRubyObject;

import org.jruby.util.ByteList;

import java.sql.SQLException;
import org.jruby.RubyObjectAdapter;
import org.jruby.anno.JRubyMethod;
import org.jruby.runtime.ThreadContext;

public class JdbcDerbySpec {
    private static RubyObjectAdapter rubyApi;
    public static void load(RubyModule jdbcSpec, RubyObjectAdapter adapter) {
        RubyModule derby = jdbcSpec.defineModuleUnder("Derby");
        derby.defineAnnotatedMethods(JdbcDerbySpec.class);
        RubyModule column = derby.defineModuleUnder("Column");
        column.defineAnnotatedMethods(Column.class);
        rubyApi = adapter;
    }

    public static class Column {
        @JRubyMethod(name = "type_cast", required = 1)
        public static IRubyObject type_cast(IRubyObject recv, IRubyObject value) {
            Ruby runtime = recv.getRuntime();

            if (value.isNil() || ((value instanceof RubyString) && value.toString().trim().equalsIgnoreCase("null"))) {
                return runtime.getNil();
            }

            String type = rubyApi.getInstanceVariable(recv, "@type").toString();

            switch (type.charAt(0)) {
                case 's': //string
                    return value;
                case 't': //text, timestamp, time
                    if (type.equals("text")) {
                        return value;
                    } else if (type.equals("timestamp")) {
                        return rubyApi.callMethod(recv.getMetaClass(), "string_to_time", value);
                    } else { //time
                        return rubyApi.callMethod(recv.getMetaClass(), "string_to_dummy_time", value);
                    }
                case 'i': //integer
                case 'p': //primary key
                    if (value.respondsTo("to_i")) {
                        return rubyApi.callMethod(value, "to_i");
                    } else {
                        return runtime.newFixnum(value.isTrue() ? 1 : 0);
                    }
                case 'd': //decimal, datetime, date
                    if (type.equals("datetime")) {
                        return rubyApi.callMethod(recv.getMetaClass(), "string_to_time", value);
                    } else if (type.equals("date")) {
                        return rubyApi.callMethod(recv.getMetaClass(), "string_to_date", value);
                    } else {
                        return rubyApi.callMethod(recv.getMetaClass(), "value_to_decimal", value);
                    }
                case 'f': //float
                    return rubyApi.callMethod(value, "to_f");
                case 'b': //binary, boolean
                    if (type.equals("binary")) {
                        return rubyApi.callMethod(recv.getMetaClass(), "binary_to_string", value);
                    } else {
                        return rubyApi.callMethod(recv.getMetaClass(), "value_to_boolean", value);
                    }
            }
            return value;
        }
    }

    @JRubyMethod(name = "quote", required = 1, optional = 1)
    public static IRubyObject quote(ThreadContext context, IRubyObject recv, IRubyObject[] args) {
        Ruby runtime = recv.getRuntime();
        IRubyObject value = args[0];
        if (args.length > 1) {
            IRubyObject col = args[1];
            String type = rubyApi.callMethod(col, "type").toString();
            if (value instanceof RubyString) {
                if (type.equals("string")) {
                    return quote_string_with_surround(runtime, "'", (RubyString)value, "'");
                } else if (type.equals("text")) {
                    return quote_string_with_surround(runtime, "CAST('", (RubyString)value, "' AS CLOB)");
                } else if (type.equals("binary")) {
                    return hexquote_string_with_surround(runtime, "CAST(X'", (RubyString)value, "' AS BLOB)");
                } else {
                    // column type :integer or other numeric or date version
                    if (only_digits((RubyString)value)) {
                        return value;
                    } else {
                        return super_quote(context, recv, runtime, value, col);
                    }
                }
            } else if ((value instanceof RubyFloat) || (value instanceof RubyFixnum) || (value instanceof RubyBignum)) {
                if (type.equals("string")) {
                    return quote_string_with_surround(runtime, "'", RubyString.objAsString(context, value), "'");
                }
            }
        } 
        return super_quote(context, recv, runtime, value, runtime.getNil());
    }

    private final static ByteList NULL = new ByteList("NULL".getBytes());

    private static IRubyObject super_quote(ThreadContext context, IRubyObject recv, Ruby runtime, IRubyObject value, IRubyObject col) {
        if (value.respondsTo("quoted_id")) {
            return rubyApi.callMethod(value, "quoted_id");
        }
        
        IRubyObject type = (col.isNil()) ? col : rubyApi.callMethod(col, "type");
        RubyModule multibyteChars = (RubyModule) 
                ((RubyModule) ((RubyModule) runtime.getModule("ActiveSupport")).getConstant("Multibyte")).getConstantAt("Chars");
        if (value instanceof RubyString || rubyApi.isKindOf(value, multibyteChars)) {
            RubyString svalue = RubyString.objAsString(context, value);
            if (type == runtime.newSymbol("binary") && col.getType().respondsTo("string_to_binary")) {
                return quote_string_with_surround(runtime, "'", (RubyString)(rubyApi.callMethod(col.getType(), "string_to_binary", svalue)), "'"); 
            } else if (type == runtime.newSymbol("integer") || type == runtime.newSymbol("float")) {
                return RubyString.objAsString(context, ((type == runtime.newSymbol("integer")) ?
                                               rubyApi.callMethod(svalue, "to_i") : 
                                               rubyApi.callMethod(svalue, "to_f")));
            } else {
                return quote_string_with_surround(runtime, "'", svalue, "'"); 
            }
        } else if (value.isNil()) {
            return runtime.newString(NULL);
        } else if (value instanceof RubyBoolean) {
            return (value.isTrue() ? 
                    (type == runtime.newSymbol(":integer")) ? runtime.newString("1") : rubyApi.callMethod(recv, "quoted_true") :
                    (type == runtime.newSymbol(":integer")) ? runtime.newString("0") : rubyApi.callMethod(recv, "quoted_false"));
        } else if((value instanceof RubyFloat) || (value instanceof RubyFixnum) || (value instanceof RubyBignum)) {
            return RubyString.objAsString(context, value);
        } else if(value instanceof RubyBigDecimal) {
            return rubyApi.callMethod(value, "to_s", runtime.newString("F"));
        } else if (rubyApi.callMethod(value, "acts_like?", runtime.newString("date")).isTrue() || rubyApi.callMethod(value, "acts_like?", runtime.newString("time")).isTrue()) {
            return quote_string_with_surround(runtime, "'", (RubyString)(rubyApi.callMethod(recv, "quoted_date", value)), "'"); 
        } else {
            return quote_string_with_surround(runtime, "'", (RubyString)(rubyApi.callMethod(value, "to_yaml")), "'");
        }
    }

    private final static ByteList TWO_SINGLE = new ByteList(new byte[]{'\'','\''});

    private static IRubyObject quote_string_with_surround(Ruby runtime, String before, RubyString string, String after) {
        ByteList input = string.getByteList();
        ByteList output = new ByteList(before.getBytes());
        for(int i = input.begin; i< input.begin + input.realSize; i++) {
            switch(input.bytes[i]) {
            case '\'':
                output.append(input.bytes[i]);
                //FALLTHROUGH
            default:
                output.append(input.bytes[i]);
            }

        }

        output.append(after.getBytes());

        return runtime.newString(output);
    }

    private final static byte[] HEX = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};

    private static IRubyObject hexquote_string_with_surround(Ruby runtime, String before, RubyString string, String after) {
        ByteList input = string.getByteList();
        ByteList output = new ByteList(before.getBytes());
        int written = 0;
        for(int i = input.begin; i< input.begin + input.realSize; i++) {
            byte b1 = input.bytes[i];
            byte higher = HEX[(((char)b1)>>4)%16];
            byte lower = HEX[((char)b1)%16];
            output.append(higher);
            output.append(lower);
            written += 2;
            if(written >= 16334) { // max hex length = 16334
              output.append("'||X'".getBytes());
              written = 0;
            }
        }

        output.append(after.getBytes());
        return runtime.newStringShared(output);
    }

    private static boolean only_digits(RubyString inp) {
        ByteList input = inp.getByteList();
        for(int i = input.begin; i< input.begin + input.realSize; i++) {
            if(input.bytes[i] < '0' || input.bytes[i] > '9') {
                return false;
            }
        }
        return true;
    }

    @JRubyMethod(name = "quote_string", required = 1)
    public static IRubyObject quote_string(IRubyObject recv, IRubyObject string) {
        boolean replacementFound = false;
        ByteList bl = ((RubyString) string).getByteList();
        
        for(int i = bl.begin; i < bl.begin + bl.realSize; i++) {
            switch (bl.bytes[i]) {
            case '\'': break;
            default: continue;
            }
            
            // On first replacement allocate a different bytelist so we don't manip original 
            if(!replacementFound) {
                i-= bl.begin;
                bl = new ByteList(bl);
                replacementFound = true;
            }

            bl.replace(i, 1, TWO_SINGLE);
            i+=1;
        }
        if(replacementFound) {
            return recv.getRuntime().newStringShared(bl);
        } else {
            return string;
        }
    }

    @JRubyMethod(name = "select_all", rest = true)
    public static IRubyObject select_all(IRubyObject recv, IRubyObject[] args) {
        return rubyApi.callMethod(recv, "execute", args);
    }

    @JRubyMethod(name = "select_one", rest = true)
    public static IRubyObject select_one(IRubyObject recv, IRubyObject[] args) {
        IRubyObject limit = rubyApi.getInstanceVariable(recv, "@limit");
        if (limit == null || limit.isNil()) {
            rubyApi.setInstanceVariable(recv, "@limit", recv.getRuntime().newFixnum(1));
        }
        try {
            IRubyObject result = rubyApi.callMethod(recv, "execute", args);
            return rubyApi.callMethod(result, "first");
        } finally {
            rubyApi.setInstanceVariable(recv, "@limit", recv.getRuntime().getNil());
        }
    }

    @JRubyMethod(name = "_execute", required = 1, optional = 1)
    public static IRubyObject _execute(ThreadContext context, IRubyObject recv, IRubyObject[] args) throws SQLException, java.io.IOException {
        Ruby runtime = recv.getRuntime();
        RubyJdbcConnection conn = (RubyJdbcConnection) rubyApi.getInstanceVariable(recv, "@connection");
        String sql = args[0].toString().trim().toLowerCase();
        if (sql.charAt(0) == '(') {
            sql = sql.substring(1).trim();
        }
        if (sql.startsWith("insert")) {
            return conn.execute_insert(context, args[0]);
        } else if (sql.startsWith("select") || sql.startsWith("show") || sql.startsWith("values")) {
            return conn.execute_query(context, args[0]);
        } else {
            return conn.execute_update(context, args[0]);
        }
    }
}
