import static com.github.forax.proxy2.MethodBuilder.methodBuilder;
import static java.lang.invoke.MethodHandles.lookup;
import static java.lang.invoke.MethodHandles.publicLookup;
import static java.lang.invoke.MethodType.methodType;

import java.lang.invoke.CallSite;
import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.Method;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

import com.github.forax.proxy2.MethodBuilder;
import com.github.forax.proxy2.Proxy2;
import com.github.forax.proxy2.Proxy2.ProxyContext;
import com.github.forax.proxy2.Proxy2.ProxyHandler;

public class ORMapper {
  public static class TransactionManager {
    private static final ThreadLocal<HashSet<Object>> transactions = ThreadLocal.withInitial(HashSet::new);

    public static void markDirty(Object o) {
      transactions.get().add(o);
    }

    public static Set<Object> getDirtySetAndClear() {
      HashSet<Object> set = transactions.get();
      transactions.remove();
      return set;
    }
  }

  final ClassValue<MethodHandle> beanFactories = new ClassValue<MethodHandle>() {
    @Override
    protected MethodHandle computeValue(Class<?> type) {
      return Proxy2.createAnonymousProxyFactory(publicLookup(), methodType(type, HashMap.class), new ProxyHandler.Default() {
        @Override
        public CallSite bootstrap(ProxyContext context) throws Throwable {
          MethodHandle target;
          Method method = context.method();
          MethodBuilder builder = methodBuilder(context.type());
          switch(method.getName()) {
          case "toString":
            target = builder
                .dropFirst()
                .convertTo(String.class, AbstractMap.class)  //FIXME
                .unreflect(publicLookup(), HashMap.class.getMethod("toString"));
            break;
          default:
            if (method.getParameterCount() == 0) { 
              target = builder                     // getter
                  .dropFirst()
                  .insertAt(1, Object.class, method.getName())
                  .convertTo(Object.class, HashMap.class, Object.class)
                  .unreflect(publicLookup(), HashMap.class.getMethod("get", Object.class));
            } else {                               
              target = builder                     // setter
                  .before(b -> b
                      .dropFirst()
                      .insertAt(1, Object.class, method.getName())
                      .convertTo(Object.class, HashMap.class, Object.class, Object.class)
                      .unreflect(publicLookup(), HashMap.class.getMethod("put", Object.class, Object.class)))
                  .dropAt(1)
                  .dropAt(1)
                  .before(b -> b
                      .unreflect(publicLookup(), TransactionManager.class.getMethod("markDirty", Object.class)))
                  .convertTo(method.getReturnType(), Object.class)
                  .callIdentity();
            }
          }
          return new ConstantCallSite(target);
        }
      });
    }
  };

  static <T> T newBean(ClassValue<MethodHandle> factories, Class<T> type) {
    return newInstance(factories, type, new HashMap<>());
  }

  private static <T> T newInstance(ClassValue<MethodHandle> factories, Class<T> type, Object... args) {
    try {
      return type.cast(factories.get(type).invokeWithArguments(args));
    } catch (RuntimeException | Error e) {
      throw e;
    } catch (Throwable e) {
      throw new UndeclaredThrowableException(e);
    }
  }

  private final ClassValue<MethodHandle> serviceFactories = new ClassValue<MethodHandle>() {
    @Override
    protected MethodHandle computeValue(Class<?> type) {
      Lookup lookup = lookup();
      return Proxy2.createAnonymousProxyFactory(publicLookup(), methodType(type), new ProxyHandler.Default() {
        @Override
        public CallSite bootstrap(ProxyContext context) throws Throwable {
          Method method = context.method();
          switch(method.getName()) {
          case "create":
            MethodHandle target = methodBuilder(context.type())
                .dropFirst()
                .insertAt(0, ClassValue.class, beanFactories)
                .insertAt(1, Class.class, method.getReturnType())
                .convertTo(Object.class, ClassValue.class, Class.class)
                .unreflect(lookup, ORMapper.class.getDeclaredMethod("newBean", ClassValue.class, Class.class));
            return new ConstantCallSite(target);
          default:
            throw new NoSuchMethodError(method.toString());
          }
        }
      });
    }
  };

  public <T> T createService(Class<T> type) {
    return newInstance(serviceFactories, type);
  }

  
  // --- example
  
  public interface SQLUser {
    public int id();
    public SQLUser id(int id);
    public String name();
    public SQLUser name(String name);

    @Override
    public String toString();  //FIXME remove when Object methods will be supported
  }

  public interface SQLService {
    public SQLUser create();
  }

  public static void main(String[] args) {
    ORMapper mapper = new ORMapper();
    SQLService sqlService = mapper.createService(SQLService.class);
    SQLUser user = sqlService.create().id(3).name("Bob");
    System.out.println(user.name());
    System.out.println(TransactionManager.getDirtySetAndClear());
  }
}