/*
 * Copyright (C) 2019 Cricin
 *
 * 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 cn.cricin.folivora.preview;

import android.content.res.Resources;
import android.content.res.Resources_Delegate;
import android.view.LayoutInflater;

import com.android.layoutlib.bridge.android.BridgeContext;
import com.intellij.openapi.diagnostic.Logger;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;

import sun.misc.Unsafe;

/**
 * Preview support entry point for Folivora.
 */
@SuppressWarnings("unchecked")
public final class FolivoraPreview {
  static final Logger sLogger = Logger.getInstance(FolivoraPreview.class);

  private static Field sContextField;
  private static WeakHashMap<Resources, BridgeContext> sContextMap;

  public static void install() {
    try {
      Field field = Resources_Delegate.class.getDeclaredField("sContexts");
      field.setAccessible(true);
      sContextMap = (WeakHashMap<Resources, BridgeContext>) field.get(null);
    } catch (Throwable t) {
      sLogger.info("Unable to find static field sContexts in" +
        " Resource_Delegate, current AS version may lower than 3.0", t);
    }
    if (sContextMap == null) {
      try {
        sContextField = Resources.class.getDeclaredField("mContext");
        sContextField.setAccessible(true);
      } catch (Throwable t) {
        sLogger.info("Unable to find static field mContext in" +
          " Resource_Delegate, current AS version may higher than 3.0", t);
      }
    }
    if (sContextField == null && sContextMap == null) {
      sLogger.info("Preview install failed, AS version not supported");
      return;
    }
    tryHookConstructorMap();
  }

  private static void tryHookConstructorMap() {
    Field field = null;
    HashMap<String, Constructor<?>> origin = null;
    boolean needHookWithUnsafe = false;
    try {
      field = LayoutInflater.class.getDeclaredField("sConstructorMap");
      field.setAccessible(true);
      Field modifiers = Field.class.getDeclaredField("modifiers");
      modifiers.setAccessible(true);
      modifiers.set(field, field.getModifiers() & ~Modifier.FINAL);
      origin = (HashMap<String, Constructor<?>>) field.get(null);
      if (origin.getClass().getCanonicalName().endsWith("MyHashMap")) return;// already hooked
      field.set(null, new MyHashMap<>(origin));
    } catch (Exception ex) {
      needHookWithUnsafe = true;
    }
    if(field == null || origin == null) {
      sLogger.info("Preview install failed, Unable to find field LayoutInflater.sConstructorMap");
      return;
    }
    // if failed, hook using unsafe
    if (needHookWithUnsafe) {
      Unsafe unsafe = getUnsafe();
      if(unsafe == null) return;
      Object fieldBase = unsafe.staticFieldBase(field);
      long offset = unsafe.staticFieldOffset(field);
      unsafe.putObjectVolatile(fieldBase, offset, new MyHashMap<>(origin));
    }
    try {
      Object o = field.get(null);
      if (o != null && o.getClass().getCanonicalName().endsWith("MyHashMap")) {
        sLogger.info("Preview installed successfully");
      } else {
        sLogger.info("Preview install failed");
      }
    } catch (Exception e) {
      sLogger.info("Preview install failed");
    }
  }

  private static Unsafe getUnsafe(){
    Unsafe unsafe = null;
    try {
      unsafe = Unsafe.getUnsafe();
    } catch (SecurityException e){
      try {
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        unsafe = (Unsafe) field.get(null);
      } catch (Exception ignore) {}
    }
    return unsafe;
  }

  private static void installViewFactoryIfNeeded() {
    Collection<BridgeContext> contexts = peekContexts();
    for (BridgeContext context : contexts) {
      LayoutInflater inflater = LayoutInflater.from(context);
      if (inflater.getFactory2() == null) {
        inflater.setFactory2(new ViewFactory(inflater, context.getLayoutlibCallback()));
      }
    }
  }

  private static Collection<BridgeContext> peekContexts() {
    if (sContextMap != null) {
      return sContextMap.values();
    } else if (sContextField != null) {
      try {
        return Collections.singletonList((BridgeContext) sContextField.get(Resources.getSystem()));
      } catch (Exception ignore) {}
    }
    return Collections.emptyList();
  }

  static class MyHashMap<K,V> extends HashMap<K,V>{
    MyHashMap(Map<? extends K, ? extends V> map) {
      super(map);
    }

    @Override
    public V get(Object o) {
      installViewFactoryIfNeeded();
      return super.get(o);
    }
  }

}