package net.wizardsoflua.lua.module.types;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static java.util.Objects.requireNonNull;

import javax.annotation.Nullable;

import net.sandius.rembulan.ByteString;
import net.sandius.rembulan.Table;
import net.sandius.rembulan.runtime.LuaFunction;
import net.wizardsoflua.lua.classes.CustomLuaClass;
import net.wizardsoflua.lua.classes.LuaClass;
import net.wizardsoflua.lua.classes.LuaClassLoader;
import net.wizardsoflua.lua.classes.ObjectClass;

public class Types {
  private static final String NIL_META = "nil";
  private static final String BOOLEAN_META = "boolean";
  private static final String FUNCTION_META = "function";
  private static final String NUMBER_META = "number";
  private static final String STRING_META = "string";
  private static final String TABLE_META = "table";

  private final LuaClassLoader classLoader;

  public Types(LuaClassLoader classLoader) {
    this.classLoader = requireNonNull(classLoader, "classLoader == null!");
  }

  public Table getEnv() {
    return classLoader.getEnv();
  }

  /**
   * Declares a new {@link LuaClass} with the specified name and the optional superclass meta table.
   *
   * @param luaClassName
   * @param superClassMetaTable
   */
  public void declareClass(String luaClassName, @Nullable Table superClassMetaTable) {
    requireNonNull(luaClassName, "luaClassName == null!");
    Table _G = classLoader.get_G();
    checkState(_G.rawget(luaClassName) == null,
        "bad argument #%s: a global variable with name '%s' is already defined", 1, luaClassName);

    LuaClass superClass;
    if (superClassMetaTable != null) {
      superClass = classLoader.getLuaClassForMetaTable(superClassMetaTable);
      checkArgument(superClass != null,
          "The table '%s' does not represent a LuaClass loaded by this LuaClassLoader",
          superClassMetaTable);
    } else {
      superClass = classLoader.getLuaClassOfType(ObjectClass.class);
    }
    classLoader.load(new CustomLuaClass(luaClassName, superClass));
  }

  /**
   * Returns <code>true</code> if the given Lua object is an instance of the Lua class represented
   * by the given class metatable.
   *
   * @param classMT
   * @param luaObj
   * @return <code>true</code> if the given Lua object is an instance of the Lua class represented
   *         by the given class metatable
   */
  public boolean isInstanceOf(Table classMT, Object luaObj) {
    if (luaObj == null) {
      return false;
    }
    if (!(luaObj instanceof Table)) {
      return false;
    }
    Table actualMT = ((Table) luaObj).getMetatable();
    return actualMT != null && (actualMT == classMT || isInstanceOf(classMT, actualMT));
  }

  /**
   * Returns the Lua type name of the given Lua object.
   *
   * @param luaObject
   * @return the Lua type name of the given Lua object
   */
  public @Nullable String getTypename(@Nullable Object luaObject) {
    if (luaObject == null) {
      return NIL_META;
    }
    if (luaObject instanceof Table) {
      Table table = (Table) luaObject;
      String classname = getClassname(table);
      if (classname != null) {
        return classname;
      }
    }
    return getTypename(luaObject.getClass());
  }

  public @Nullable String getTypename(Class<?> type) {
    if (Table.class.isAssignableFrom(type)) {
      return TABLE_META;
    }
    if (ByteString.class.isAssignableFrom(type) || String.class.isAssignableFrom(type)) {
      return STRING_META;
    }
    if (Number.class.isAssignableFrom(type)) {
      return NUMBER_META;
    }
    if (Boolean.class.isAssignableFrom(type)) {
      return BOOLEAN_META;
    }
    if (LuaFunction.class.isAssignableFrom(type)) {
      return FUNCTION_META;
    }
    return type.getSimpleName();
  }

  /**
   * Returns the name of the {@link LuaClass} of the specified {@link Table} or {@code null} if the
   * {@link Table} is not an instance of a {@link LuaClass}.
   *
   * @param table
   * @return the name of the {@link LuaClass} of the specified {@link Table} or {@code null}
   */
  public @Nullable String getClassname(Table table) {
    Table metatable = table.getMetatable();
    if (metatable != null) {
      LuaClass luaClass = classLoader.getLuaClassForMetaTable(metatable);
      if (luaClass != null) {
        return luaClass.getName();
      }
    }
    return null;
  }
}

@@ -2143,33 +2143,28 @@
 rns 
-%3C
+%7B@
 code
-%3E
+ 
 true
-%3C/code%3E
+%7D
  if the 
 give
@@ -2151,37 +2151,41 @@
 de true%7D if the 
-given
+specified
  Lua object is a
@@ -2227,26 +2227,30 @@
 nted
+ by
 %0A   *
- by
  the 
-given
+specified
  cla
@@ -2252,24 +2252,25 @@
 d class meta
+ 
 table.%0A   *%0A
@@ -2287,17 +2287,24 @@
 m classM
-T
+etaTable
 %0A   * @p
@@ -2314,16 +2314,19 @@
 m luaObj
+ect
 %0A   * @r
@@ -2335,33 +2335,28 @@
 urn 
-%3C
+%7B@
 code
-%3E
+ 
 true
-%3C/code%3E
+%7D
  if the 
 give
@@ -2347,29 +2347,33 @@
 rue%7D if the 
-given
+specified
  Lua object 
@@ -2407,62 +2407,8 @@
 lass
- represented%0A   *         by the given class metatable
 %0A   
@@ -2452,17 +2452,24 @@
 e classM
-T
+etaTable
 , Object
@@ -2475,16 +2475,19 @@
 t luaObj
+ect
 ) %7B%0A    
@@ -2496,16 +2496,19 @@
  (luaObj
+ect
  == null
@@ -2553,16 +2553,19 @@
 !(luaObj
+ect
  instanc
@@ -2621,17 +2621,24 @@
  actualM
-T
+etaTable
  = ((Tab
@@ -2647,16 +2647,19 @@
 ) luaObj
+ect
 ).getMet
@@ -2690,17 +2690,32 @@
 ualM
-T != null
+etaTable != null%0A       
  && 
@@ -2722,17 +2722,24 @@
 (actualM
-T
+etaTable
  == clas
@@ -2740,17 +2740,24 @@
 = classM
-T
+etaTable
  %7C%7C isIn
@@ -2771,17 +2771,24 @@
 f(classM
-T
+etaTable
 , actual
@@ -2788,17 +2788,24 @@
  actualM
-T
+etaTable
 ));%0A  %7D%0A

Refactor Types.isInstanceOf