/**
 * Copyright (c) 2012-2015 Edgar Espina
 * <p>
 * This file is part of Handlebars.java.
 * <p>
 * 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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.ibizlab.codegen.templating.handlebars;

import cn.ibizlab.codegen.model.BaseModel;
import cn.ibizlab.codegen.model.IModel;
import com.github.jknack.handlebars.ValueResolver;
import com.github.jknack.handlebars.context.MethodValueResolver;

import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

import static org.apache.commons.lang3.Validate.notNull;

/**
 * A JavaBean method value resolver.
 *
 * @author edgar.espina
 * @since 0.1.1
 */
public class BaseModelValueResolver implements ValueResolver {


  /**
   * A concurrent and thread-safe cache for {@link Member}.
   */
  private final Map<Class<?>, Map<String, Method>> cache = new ConcurrentHashMap<>();

  @Override
  public final Object resolve(final Object context,  String name) {
    Class<?> key = context.getClass();
    Map<String, Method> mcache = cache(key);

    Method member = mcache.get(name);
    if (member == null) {
      if(context instanceof IModel)
      {
        mcache = cache(((IModel) context).getOpt().getClass());
        member = mcache.get(name);
        if (member == null) {
          return UNRESOLVED;
        } else {
          return invokeMember(member, ((IModel) context).getOpt());
        }
      }
      return UNRESOLVED;
    } else {
      return invokeMember(member, context);
    }
  }

  @Override
  public Object resolve(final Object context) {
    return UNRESOLVED;
  }

  /**
   * Get or build a class member cache.
   *
   * @param clazz Owner/key.
   * @return A class cache.
   */
  private Map<String, Method> cache(final Class<?> clazz) {
    Map<String, Method> mcache = this.cache.get(clazz);
    if (mcache == null) {
      mcache = new HashMap<>();
      Set<Method> members = members(clazz);
      for (Method m : members) {
        // Mark as accessible.
        if (m instanceof AccessibleObject) {
          ((AccessibleObject) m).setAccessible(true);
        }
        String name=memberName(m);

        mcache.put(name, m);
        if(name.startsWith("pS"))
        {
          mcache.put("ps"+name.substring(2),m);
        }
        mcache.put(m.getName(),m);
      }
      this.cache.put(clazz, mcache);
    }
    return mcache;
  }


  /**
   * True if the member is public.
   *
   * @param member The member object.
   * @return True if the member is public.
   */
  protected boolean isPublic(final Method member) {
    return Modifier.isPublic(member.getModifiers());
  }

  /**
   * True if the member is private.
   *
   * @param member The member object.
   * @return True if the member is private.
   */
  protected boolean isPrivate(final Method member) {
    return Modifier.isPrivate(member.getModifiers());
  }

  /**
   * True if the member is protected.
   *
   * @param member The member object.
   * @return True if the member is protected.
   */
  protected boolean isProtected(final Method member) {
    return Modifier.isProtected(member.getModifiers());
  }

  /**
   * True if the member is static.
   *
   * @param member The member object.
   * @return True if the member is static.
   */
  protected boolean isStatic(final Method member) {
    return Modifier.isStatic(member.getModifiers());
  }



  @Override
  public Set<Map.Entry<String, Object>> propertySet(final Object context) {
    notNull(context, "The context is required.");
    if (context instanceof Map) {
      return Collections.emptySet();
    } else if (context instanceof Collection) {
      return Collections.emptySet();
    }
    Collection<Method> members = cache(context.getClass()).values();
    Map<String, Object> propertySet = new LinkedHashMap<>();
    for (Method member : members) {
      String name = memberName(member);
      propertySet.put(name, resolve(context, name));
    }
    return propertySet.entrySet();
  }



  /**
   * Args for getters.
   */
  private static final Object[] EMPTY_ARGS = new Object[0];


  protected Object invokeMember(final Method member, final Object context) {
    try {
      return member.invoke(context, EMPTY_ARGS);
    } catch (InvocationTargetException ex) {
      Throwable cause = ex.getCause();
      if (cause instanceof RuntimeException) {
        throw (RuntimeException) cause;
      }
      throw new IllegalStateException("Execution of '" + member.getName() + "' failed", cause);
    } catch (IllegalAccessException ex) {
      throw new IllegalStateException(
              "Could not access method:  '" + member.getName() + "'", ex);
    }
  }

  protected Set<Method> members(final Class<?> clazz) {
    Set<Method> members = new LinkedHashSet<>();
    members(clazz, members);
    return members;
  }


  /**
   * The 'is' prefix.
   */
  private static final String IS_PREFIX = "is";

  /**
   * The 'get' prefix.
   */
  private static final String GET_PREFIX = "get";

  /**
   * The default value resolver.
   */
  public static final ValueResolver INSTANCE = new BaseModelValueResolver();

  public boolean matches(final Method method, final String name) {
    if (name.equals("length") && method.getName().equals("size")) {
      boolean isCollection = isCollectionMethod(method);
      if (isCollection) {
        return true;
      }
    }

    boolean isStatic = isStatic(method);
    boolean isPublic = isPublic(method);
    boolean isGet = method.getName().equals(javaBeanMethod(GET_PREFIX, name));
    boolean isBoolGet = method.getName().equals(javaBeanMethod(IS_PREFIX, name));
    int parameterCount = method.getParameterTypes().length;

    return !isStatic && isPublic && parameterCount == 0 && (isGet || isBoolGet);
  }

  /**
   * Convert the property's name to a JavaBean read method name.
   *
   * @param prefix The prefix: 'get' or 'is'.
   * @param name   The unqualified property name.
   * @return The javaBean method name.
   */
  private static String javaBeanMethod(final String prefix,
      final String name) {
    StringBuilder buffer = new StringBuilder(prefix);
    buffer.append(name);
    buffer.setCharAt(prefix.length(), Character.toUpperCase(name.charAt(0)));
    return buffer.toString();
  }

  protected String memberName(final Method member) {
    if (member.getName().equals("size")) {
      boolean isCollection = isCollectionMethod(member);

      if (isCollection) {
        return "length";
      }
    }

    String name = member.getName();
    if (name.startsWith(GET_PREFIX)) {
      name = name.substring(GET_PREFIX.length());
    } else if (name.startsWith(IS_PREFIX)) {
      name = name.substring(IS_PREFIX.length());
    } else {
      return name;
    }
    if (name.length() > 0) {
      return Character.toLowerCase(name.charAt(0)) + name.substring(1);
    }
    return member.getName();
  }

  /**
   * Collect all the method from the given class.
   *
   * @param clazz The base class.
   * @param members The members result set.
   */
  protected void members(final Class<?> clazz, final Set<Method> members) {
    if (clazz != Object.class) {
      // Keep backing up the inheritance hierarchy.
      Method[] methods = clazz.getDeclaredMethods();
      for (Method method : methods) {
        if (matches(method, memberName(method))) {
          members.add(method);
        }
      }
      if (clazz.getSuperclass() != null) {
        members(clazz.getSuperclass(), members);
      }
      for (Class<?> superIfc : clazz.getInterfaces()) {
        members(superIfc, members);
      }
    }
  }

  /**
   * Check is method class implements Collection interface.
   *
   * @param method from class
   * @return true/false
   */
  private boolean isCollectionMethod(final Method method) {
    for (Class clazz : method.getDeclaringClass().getInterfaces()) {
      if (Collection.class.equals(clazz)) {
        return true;
      }
    }
    return false;
  }
}
